Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading