Skip to content

Commit 2484292

Browse files
bashirpartoviBashir Partovi
andauthored
Decoupled ScenarioMetadata and InitializerMetadata from Identity by introducing RegistryEntry (microsoft#1370)
Co-authored-by: Bashir Partovi <bpartovi@microsoft.com>
1 parent a1ebb12 commit 2484292

7 files changed

Lines changed: 61 additions & 175 deletions

File tree

pyrit/registry/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
"""Registry module for PyRIT class and instance registries."""
55

6-
from pyrit.identifiers import Identifier, class_name_to_snake_case, snake_case_to_class_name
76
from pyrit.registry.base import RegistryProtocol
87
from pyrit.registry.class_registries import (
98
BaseClassRegistry,
@@ -28,15 +27,12 @@
2827
"BaseClassRegistry",
2928
"BaseInstanceRegistry",
3029
"ClassEntry",
31-
"class_name_to_snake_case",
3230
"discover_in_directory",
3331
"discover_in_package",
3432
"discover_subclasses_in_loaded_modules",
35-
"Identifier",
3633
"InitializerMetadata",
3734
"InitializerRegistry",
3835
"RegistryProtocol",
39-
"snake_case_to_class_name",
4036
"ScenarioMetadata",
4137
"ScenarioRegistry",
4238
"ScorerRegistry",

pyrit/registry/base.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,43 @@
88
and instance registries (which store T instances).
99
"""
1010

11+
from dataclasses import dataclass
1112
from typing import Any, Dict, Iterator, List, Optional, Protocol, TypeVar, runtime_checkable
1213

14+
from pyrit.identifiers.class_name_utils import class_name_to_snake_case
15+
1316
# Type variable for metadata (invariant for Protocol compatibility)
1417
MetadataT = TypeVar("MetadataT")
1518

1619

20+
@dataclass(frozen=True)
21+
class ClassRegistryEntry:
22+
"""
23+
Minimal base for class-level registry metadata.
24+
25+
Provides the common fields every registry metadata type needs for display,
26+
lookup, and filtering in class registries.
27+
28+
Attributes:
29+
class_name (str): Python class name (e.g., "ContentHarmsScenario").
30+
class_module (str): Full module path (e.g., "pyrit.scenario.scenarios.content_harms").
31+
class_description (str): Human-readable description, typically from the class docstring.
32+
"""
33+
34+
class_name: str
35+
class_module: str
36+
class_description: str = ""
37+
38+
@property
39+
def snake_class_name(self) -> str:
40+
"""
41+
Snake_case version of class_name (e.g., "content_harms_scenario").
42+
43+
Used by CLI formatting and as registry display keys.
44+
"""
45+
return class_name_to_snake_case(self.class_name)
46+
47+
1748
@runtime_checkable
1849
class RegistryProtocol(Protocol[MetadataT]):
1950
"""

pyrit/registry/class_registries/base_class_registry.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from abc import ABC, abstractmethod
2020
from typing import Callable, Dict, Generic, Iterator, List, Optional, Type, TypeVar
2121

22-
from pyrit.identifiers import Identifier
2322
from pyrit.identifiers.class_name_utils import class_name_to_snake_case
2423
from pyrit.registry.base import RegistryProtocol
2524

@@ -183,36 +182,6 @@ def _build_metadata(self, name: str, entry: ClassEntry[T]) -> MetadataT:
183182
"""
184183
pass
185184

186-
def _build_base_metadata(self, name: str, entry: ClassEntry[T]) -> Identifier:
187-
"""
188-
Build the common base metadata for a registered class.
189-
190-
This helper extracts fields common to all registries: name, class_name, class_description.
191-
Subclasses can use this for building common fields if needed.
192-
193-
Args:
194-
name: The registry name (snake_case identifier).
195-
entry: The ClassEntry containing the registered class.
196-
197-
Returns:
198-
An Identifier dataclass with common fields.
199-
"""
200-
registered_class = entry.registered_class
201-
202-
# Extract description from docstring, clean up whitespace
203-
doc = registered_class.__doc__ or ""
204-
if doc:
205-
description = " ".join(doc.split())
206-
else:
207-
description = entry.description or "No description available"
208-
209-
return Identifier(
210-
identifier_type="class",
211-
class_name=registered_class.__name__,
212-
class_module=registered_class.__module__,
213-
class_description=description,
214-
)
215-
216185
def get_class(self, name: str) -> Type[T]:
217186
"""
218187
Get a registered class by name.

pyrit/registry/class_registries/initializer_registry.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from pathlib import Path
1717
from typing import TYPE_CHECKING, Dict, Optional
1818

19-
from pyrit.identifiers import Identifier
19+
from pyrit.registry.base import ClassRegistryEntry
2020
from pyrit.registry.class_registries.base_class_registry import (
2121
BaseClassRegistry,
2222
ClassEntry,
@@ -34,15 +34,20 @@
3434

3535

3636
@dataclass(frozen=True)
37-
class InitializerMetadata(Identifier):
37+
class InitializerMetadata(ClassRegistryEntry):
3838
"""
3939
Metadata describing a registered PyRITInitializer class.
4040
4141
Use get_class() to get the actual class.
4242
"""
4343

44+
# Human-readable display name (e.g., "Objective Target Setup").
4445
display_name: str = field(kw_only=True)
46+
47+
# Environment variables required by the initializer.
4548
required_env_vars: tuple[str, ...] = field(kw_only=True)
49+
50+
# Execution order priority (lower = earlier).
4651
execution_order: int = field(kw_only=True)
4752

4853

@@ -208,7 +213,6 @@ def _build_metadata(self, name: str, entry: ClassEntry["PyRITInitializer"]) -> I
208213
try:
209214
instance = initializer_class()
210215
return InitializerMetadata(
211-
identifier_type="class",
212216
class_name=initializer_class.__name__,
213217
class_module=initializer_class.__module__,
214218
class_description=instance.description,
@@ -219,7 +223,6 @@ def _build_metadata(self, name: str, entry: ClassEntry["PyRITInitializer"]) -> I
219223
except Exception as e:
220224
logger.warning(f"Failed to get metadata for {name}: {e}")
221225
return InitializerMetadata(
222-
identifier_type="class",
223226
class_name=initializer_class.__name__,
224227
class_module=initializer_class.__module__,
225228
class_description="Error loading initializer metadata",

pyrit/registry/class_registries/scenario_registry.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
from pathlib import Path
1616
from typing import TYPE_CHECKING, Optional
1717

18-
from pyrit.identifiers import Identifier
1918
from pyrit.identifiers.class_name_utils import class_name_to_snake_case
19+
from pyrit.registry.base import ClassRegistryEntry
2020
from pyrit.registry.class_registries.base_class_registry import (
2121
BaseClassRegistry,
2222
ClassEntry,
@@ -33,17 +33,26 @@
3333

3434

3535
@dataclass(frozen=True)
36-
class ScenarioMetadata(Identifier):
36+
class ScenarioMetadata(ClassRegistryEntry):
3737
"""
3838
Metadata describing a registered Scenario class.
3939
4040
Use get_class() to get the actual class.
4141
"""
4242

43+
# The default strategy name (e.g., "single_turn")
4344
default_strategy: str = field(kw_only=True)
45+
46+
# All available strategy names for this scenario.
4447
all_strategies: tuple[str, ...] = field(kw_only=True)
48+
49+
# Aggregate strategies that combine multiple attack approaches.
4550
aggregate_strategies: tuple[str, ...] = field(kw_only=True)
51+
52+
# Default dataset names used by this scenario.
4653
default_datasets: tuple[str, ...] = field(kw_only=True)
54+
55+
# Maximum number of items per dataset.
4756
max_dataset_size: Optional[int] = field(kw_only=True)
4857

4958

@@ -131,7 +140,7 @@ def discover_user_scenarios(self) -> None:
131140
from pyrit.scenario.core import Scenario
132141

133142
try:
134-
for module_name, scenario_class in discover_subclasses_in_loaded_modules(
143+
for _, scenario_class in discover_subclasses_in_loaded_modules(
135144
base_class=Scenario # type: ignore[type-abstract]
136145
):
137146
# Check if this is a user-defined class (not from pyrit.scenario.scenarios)
@@ -170,7 +179,6 @@ def _build_metadata(self, name: str, entry: ClassEntry["Scenario"]) -> ScenarioM
170179
max_dataset_size = dataset_config.max_dataset_size
171180

172181
return ScenarioMetadata(
173-
identifier_type="class",
174182
class_name=scenario_class.__name__,
175183
class_module=scenario_class.__module__,
176184
class_description=description,

tests/unit/cli/test_frontend_core.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,6 @@ async def test_print_scenarios_list_with_scenarios(self, capsys):
327327
mock_registry = MagicMock()
328328
mock_registry.list_metadata.return_value = [
329329
ScenarioMetadata(
330-
identifier_type="class",
331330
class_name="TestScenario",
332331
class_module="test.scenarios",
333332
class_description="Test description",
@@ -369,7 +368,6 @@ async def test_print_initializers_list_with_initializers(self, capsys):
369368
mock_registry = MagicMock()
370369
mock_registry.list_metadata.return_value = [
371370
InitializerMetadata(
372-
identifier_type="class",
373371
class_name="TestInit",
374372
class_module="test.initializers",
375373
class_description="Test initializer",
@@ -410,7 +408,6 @@ def test_format_scenario_metadata_basic(self, capsys):
410408
"""Test format_scenario_metadata with basic metadata."""
411409

412410
scenario_metadata = ScenarioMetadata(
413-
identifier_type="class",
414411
class_name="TestScenario",
415412
class_module="test.scenarios",
416413
class_description="",
@@ -432,7 +429,6 @@ def test_format_scenario_metadata_with_description(self, capsys):
432429
"""Test format_scenario_metadata with description."""
433430

434431
scenario_metadata = ScenarioMetadata(
435-
identifier_type="class",
436432
class_name="TestScenario",
437433
class_module="test.scenarios",
438434
class_description="This is a test scenario",
@@ -451,7 +447,6 @@ def test_format_scenario_metadata_with_description(self, capsys):
451447
def test_format_scenario_metadata_with_strategies(self, capsys):
452448
"""Test format_scenario_metadata with strategies."""
453449
scenario_metadata = ScenarioMetadata(
454-
identifier_type="class",
455450
class_name="TestScenario",
456451
class_module="test.scenarios",
457452
class_description="",
@@ -472,7 +467,6 @@ def test_format_scenario_metadata_with_strategies(self, capsys):
472467
def test_format_initializer_metadata_basic(self, capsys) -> None:
473468
"""Test format_initializer_metadata with basic metadata."""
474469
initializer_metadata = InitializerMetadata(
475-
identifier_type="class",
476470
class_name="TestInit",
477471
class_module="test.initializers",
478472
class_description="",
@@ -491,7 +485,6 @@ def test_format_initializer_metadata_basic(self, capsys) -> None:
491485
def test_format_initializer_metadata_with_env_vars(self, capsys) -> None:
492486
"""Test format_initializer_metadata with environment variables."""
493487
initializer_metadata = InitializerMetadata(
494-
identifier_type="class",
495488
class_name="TestInit",
496489
class_module="test.initializers",
497490
class_description="",
@@ -509,7 +502,6 @@ def test_format_initializer_metadata_with_env_vars(self, capsys) -> None:
509502
def test_format_initializer_metadata_with_description(self, capsys) -> None:
510503
"""Test format_initializer_metadata with description."""
511504
initializer_metadata = InitializerMetadata(
512-
identifier_type="class",
513505
class_name="TestInit",
514506
class_module="test.initializers",
515507
class_description="Test description",

0 commit comments

Comments
 (0)