Skip to content
24 changes: 16 additions & 8 deletions .github/instructions/style-guide.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,22 @@ paths stay fast.

### Lazy `__init__.py` Exports (PEP 562)

Public API packages (`pyrit.prompt_target`, `pyrit.converter`, `pyrit.score`)
use `__getattr__`-based lazy loading so heavy symbols can be imported from the
package without paying the cost at package load time. See
`pyrit/prompt_target/__init__.py` for the canonical example. Rules:

- Lazy names must remain in `__all__` and have a `TYPE_CHECKING` import for IDE support.
- Internal utility packages (e.g., `pyrit.common`) simply omit heavy submodules
from `__init__.py` — consumers import directly from the specific file.
Public API packages use `__getattr__`-based lazy loading so package imports do
not load every implementation module. Use `pyrit.common.lazy_imports` and follow
the standard contract:

- Add each public export to `_LAZY_EXPORTS`, which is the runtime source of truth.
- Use a module string when the public and source attribute names match. Use the
tuple form only for aliases or module-valued exports.
- Set `__all__ = list(_LAZY_EXPORTS)`.
- Put every public export under `if TYPE_CHECKING:` for editor and static-analysis support.
- Implement `__getattr__` with `resolve_lazy_export` and `__dir__` with `get_lazy_dir`.
- Do not add eager implementation imports to package initializers.

The package contract tests in `tests/unit/common/test_lazy_package_imports.py`
enforce these rules. Internal heavy submodules that are not public exports
should remain omitted from `__init__.py`; consumers import them from the
specific module.

### Import Paths

Expand Down
72 changes: 60 additions & 12 deletions pyrit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,60 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

__name__ = "pyrit"
# Remove dev suffix when releasing and keep in sync with pyproject.toml
# NOTE: __version__ must be set before imports below to avoid circular import issues.
# Submodules (e.g., component_identifier, memory_models) reference pyrit.__version__
# and get imported transitively during the .common import chain.
Comment thread
rlundeen2 marked this conversation as resolved.
__version__ = "1.1.0.dev0"

from .common import turn_off_transformers_warning # noqa: F401
from .show_versions import show_versions # noqa: F401
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
# ruff: noqa: F401

"""PyRIT public package API."""

import os
import sys
from types import ModuleType
from typing import TYPE_CHECKING

from pyrit.common.lazy_imports import get_lazy_dir, resolve_lazy_export

if TYPE_CHECKING:
import pyrit.common.turn_off_transformers_warning as turn_off_transformers_warning
from pyrit._version import __version__
from pyrit.show_versions import show_versions

# Most people install PyRIT without torch, so suppress the transformers advisory
# before any PyRIT submodule can import transformers.
os.environ["TRANSFORMERS_NO_ADVISORY_WARNINGS"] = "True"

_LAZY_EXPORTS: dict[str, str | tuple[str, str | None]] = {
"__version__": "pyrit._version",
"show_versions": "pyrit.show_versions",
"turn_off_transformers_warning": ("pyrit.common.turn_off_transformers_warning", None),
}

__all__ = list(_LAZY_EXPORTS)


class _LazyPyRITModule(ModuleType):
"""Resolve exports that share a name with an imported child module."""

def __getattribute__(self, name: str) -> object:
if name == "show_versions":
module_globals = ModuleType.__getattribute__(self, "__dict__")
return resolve_lazy_export(
name=name,
module_name=__name__,
module_globals=module_globals,
exports=_LAZY_EXPORTS,
)
return ModuleType.__getattribute__(self, name)


sys.modules[__name__].__class__ = _LazyPyRITModule


def __getattr__(name: str) -> object:
return resolve_lazy_export(
name=name,
module_name=__name__,
module_globals=globals(),
exports=_LAZY_EXPORTS,
)


def __dir__() -> list[str]:
return get_lazy_dir(module_globals=globals(), exports=_LAZY_EXPORTS)
10 changes: 10 additions & 0 deletions pyrit/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""PyRIT package version."""

# Keep this module dependency-free to avoid circular imports. Submodules such
# as component identifiers and memory models reference ``pyrit.__version__``
# and can be imported transitively while the package is still initializing.
# Remove the development suffix when releasing and keep this value in sync with pyproject.toml.
__version__ = "1.1.0.dev0"
55 changes: 34 additions & 21 deletions pyrit/analytics/__init__.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,39 @@
# Copyright (c) Microsoft Corporation.
Comment thread
rlundeen2 marked this conversation as resolved.
# Licensed under the MIT license.
# ruff: noqa: F401

"""Analytics module for PyRIT conversation and result analysis."""

from pyrit.analytics.conversation_analytics import ConversationAnalytics
from pyrit.analytics.result_analysis import (
AttackStats,
analyze_results,
get_cached_results_for_technique,
)
from pyrit.analytics.text_matching import (
ApproximateTextMatching,
ExactTextMatching,
TextMatching,
)

__all__ = [
"analyze_results",
"ApproximateTextMatching",
"AttackStats",
"ConversationAnalytics",
"ExactTextMatching",
"get_cached_results_for_technique",
"TextMatching",
]
from typing import TYPE_CHECKING

from pyrit.common.lazy_imports import get_lazy_dir, resolve_lazy_export

if TYPE_CHECKING:
from pyrit.analytics.conversation_analytics import ConversationAnalytics
from pyrit.analytics.result_analysis import AttackStats, analyze_results, get_cached_results_for_technique
from pyrit.analytics.text_matching import ApproximateTextMatching, ExactTextMatching, TextMatching

_LAZY_EXPORTS: dict[str, str | tuple[str, str | None]] = {
"analyze_results": "pyrit.analytics.result_analysis",
"ApproximateTextMatching": "pyrit.analytics.text_matching",
"AttackStats": "pyrit.analytics.result_analysis",
"ConversationAnalytics": "pyrit.analytics.conversation_analytics",
"ExactTextMatching": "pyrit.analytics.text_matching",
"get_cached_results_for_technique": "pyrit.analytics.result_analysis",
"TextMatching": "pyrit.analytics.text_matching",
}

__all__ = list(_LAZY_EXPORTS)


def __getattr__(name: str) -> object:
return resolve_lazy_export(
name=name,
module_name=__name__,
module_globals=globals(),
exports=_LAZY_EXPORTS,
)


def __dir__() -> list[str]:
return get_lazy_dir(module_globals=globals(), exports=_LAZY_EXPORTS)
105 changes: 63 additions & 42 deletions pyrit/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,63 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""
Authentication functionality for a variety of services.
"""

from pyrit.auth.authenticator import Authenticator
from pyrit.auth.azure_auth import (
AsyncTokenProviderCredential,
AzureAuth,
TokenProviderCredential,
ensure_async_token_provider,
get_azure_async_token_provider,
get_azure_openai_auth,
get_azure_token_provider,
get_default_azure_scope,
is_azure_ml_endpoint,
is_azure_openai_endpoint,
)
from pyrit.auth.azure_storage_auth import AzureStorageAuth
from pyrit.auth.copilot_authenticator import CopilotAuthenticator
from pyrit.auth.manual_copilot_authenticator import ManualCopilotAuthenticator
from pyrit.auth.openai_auth import resolve_openai_auth

__all__ = [
"AsyncTokenProviderCredential",
"Authenticator",
"AzureAuth",
"AzureStorageAuth",
"CopilotAuthenticator",
"ManualCopilotAuthenticator",
"resolve_openai_auth",
"TokenProviderCredential",
"ensure_async_token_provider",
"get_azure_token_provider",
"get_azure_async_token_provider",
"get_default_azure_scope",
"get_azure_openai_auth",
"is_azure_ml_endpoint",
"is_azure_openai_endpoint",
]
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
# ruff: noqa: F401

"""
Authentication functionality for a variety of services.
"""

from typing import TYPE_CHECKING

from pyrit.common.lazy_imports import get_lazy_dir, resolve_lazy_export

if TYPE_CHECKING:
from pyrit.auth.authenticator import Authenticator
from pyrit.auth.azure_auth import (
AsyncTokenProviderCredential,
AzureAuth,
TokenProviderCredential,
ensure_async_token_provider,
get_azure_async_token_provider,
get_azure_openai_auth,
get_azure_token_provider,
get_default_azure_scope,
is_azure_ml_endpoint,
is_azure_openai_endpoint,
)
from pyrit.auth.azure_storage_auth import AzureStorageAuth
from pyrit.auth.copilot_authenticator import CopilotAuthenticator
from pyrit.auth.manual_copilot_authenticator import ManualCopilotAuthenticator
from pyrit.auth.openai_auth import resolve_openai_auth

_LAZY_EXPORTS: dict[str, str | tuple[str, str | None]] = {
"AsyncTokenProviderCredential": "pyrit.auth.azure_auth",
"Authenticator": "pyrit.auth.authenticator",
"AzureAuth": "pyrit.auth.azure_auth",
"AzureStorageAuth": "pyrit.auth.azure_storage_auth",
"CopilotAuthenticator": "pyrit.auth.copilot_authenticator",
"ManualCopilotAuthenticator": "pyrit.auth.manual_copilot_authenticator",
"resolve_openai_auth": "pyrit.auth.openai_auth",
"TokenProviderCredential": "pyrit.auth.azure_auth",
"ensure_async_token_provider": "pyrit.auth.azure_auth",
"get_azure_token_provider": "pyrit.auth.azure_auth",
"get_azure_async_token_provider": "pyrit.auth.azure_auth",
"get_default_azure_scope": "pyrit.auth.azure_auth",
"get_azure_openai_auth": "pyrit.auth.azure_auth",
"is_azure_ml_endpoint": "pyrit.auth.azure_auth",
"is_azure_openai_endpoint": "pyrit.auth.azure_auth",
}

__all__ = list(_LAZY_EXPORTS)


def __getattr__(name: str) -> object:
return resolve_lazy_export(
name=name,
module_name=__name__,
module_globals=globals(),
exports=_LAZY_EXPORTS,
)


def __dir__() -> list[str]:
return get_lazy_dir(module_globals=globals(), exports=_LAZY_EXPORTS)
63 changes: 40 additions & 23 deletions pyrit/backend/mappers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
# ruff: noqa: F401

"""
Backend mappers module.
Expand All @@ -8,26 +9,42 @@
Centralizes all translation logic so domain models can evolve independently of the API contract.
"""

from pyrit.backend.mappers._preview import format_last_message_preview
from pyrit.backend.mappers.attack_mappers import (
attack_result_to_summary_async,
pyrit_messages_to_dto_async,
request_piece_to_pyrit_message_piece,
request_to_pyrit_message,
)
from pyrit.backend.mappers.converter_mappers import (
converter_object_to_instance,
)
from pyrit.backend.mappers.target_mappers import (
target_object_to_instance,
)

__all__ = [
"attack_result_to_summary_async",
"converter_object_to_instance",
"format_last_message_preview",
"pyrit_messages_to_dto_async",
"request_piece_to_pyrit_message_piece",
"request_to_pyrit_message",
"target_object_to_instance",
]
from typing import TYPE_CHECKING

from pyrit.common.lazy_imports import get_lazy_dir, resolve_lazy_export

if TYPE_CHECKING:
from pyrit.backend.mappers._preview import format_last_message_preview
from pyrit.backend.mappers.attack_mappers import (
attack_result_to_summary_async,
pyrit_messages_to_dto_async,
request_piece_to_pyrit_message_piece,
request_to_pyrit_message,
)
from pyrit.backend.mappers.converter_mappers import converter_object_to_instance
from pyrit.backend.mappers.target_mappers import target_object_to_instance

_LAZY_EXPORTS: dict[str, str | tuple[str, str | None]] = {
"attack_result_to_summary_async": "pyrit.backend.mappers.attack_mappers",
"converter_object_to_instance": "pyrit.backend.mappers.converter_mappers",
"format_last_message_preview": "pyrit.backend.mappers._preview",
"pyrit_messages_to_dto_async": "pyrit.backend.mappers.attack_mappers",
"request_piece_to_pyrit_message_piece": "pyrit.backend.mappers.attack_mappers",
"request_to_pyrit_message": "pyrit.backend.mappers.attack_mappers",
"target_object_to_instance": "pyrit.backend.mappers.target_mappers",
}

__all__ = list(_LAZY_EXPORTS)


def __getattr__(name: str) -> object:
return resolve_lazy_export(
name=name,
module_name=__name__,
module_globals=globals(),
exports=_LAZY_EXPORTS,
)


def __dir__() -> list[str]:
return get_lazy_dir(module_globals=globals(), exports=_LAZY_EXPORTS)
33 changes: 29 additions & 4 deletions pyrit/backend/middleware/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
# ruff: noqa: F401

"""Middleware module for backend."""

from pyrit.backend.middleware.error_handlers import register_error_handlers
from pyrit.backend.middleware.request_id import RequestIdMiddleware
from pyrit.backend.middleware.security_headers import SecurityHeadersMiddleware
from typing import TYPE_CHECKING

__all__ = ["register_error_handlers", "RequestIdMiddleware", "SecurityHeadersMiddleware"]
from pyrit.common.lazy_imports import get_lazy_dir, resolve_lazy_export

if TYPE_CHECKING:
from pyrit.backend.middleware.error_handlers import register_error_handlers
from pyrit.backend.middleware.request_id import RequestIdMiddleware
from pyrit.backend.middleware.security_headers import SecurityHeadersMiddleware

_LAZY_EXPORTS: dict[str, str | tuple[str, str | None]] = {
"register_error_handlers": "pyrit.backend.middleware.error_handlers",
"RequestIdMiddleware": "pyrit.backend.middleware.request_id",
"SecurityHeadersMiddleware": "pyrit.backend.middleware.security_headers",
}

__all__ = list(_LAZY_EXPORTS)


def __getattr__(name: str) -> object:
return resolve_lazy_export(
name=name,
module_name=__name__,
module_globals=globals(),
exports=_LAZY_EXPORTS,
)


def __dir__() -> list[str]:
return get_lazy_dir(module_globals=globals(), exports=_LAZY_EXPORTS)
Loading