From 11101862b2434a49e4167c864e5d0eca8ff30dcc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:53:27 +0000 Subject: [PATCH 1/3] Initial plan From 0aa02731386890b75fe4dab5cf214c878e207ad6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:58:54 +0000 Subject: [PATCH 2/3] fix docstrings to satisfy check-no-rest-roles hook --- pyrit/auxiliary_attacks/gcg/__init__.py | 4 ++-- pyrit/auxiliary_attacks/gcg/config.py | 14 +++++++------- pyrit/auxiliary_attacks/gcg/data.py | 4 ++-- pyrit/auxiliary_attacks/gcg/experiments/run.py | 8 ++++---- pyrit/models/conversation_reference.py | 4 ++-- pyrit/models/retry_event.py | 4 ++-- .../initializers/components/scenario_techniques.py | 12 ++++++------ 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/pyrit/auxiliary_attacks/gcg/__init__.py b/pyrit/auxiliary_attacks/gcg/__init__.py index bff04bc1ec..5a4c67e0fc 100644 --- a/pyrit/auxiliary_attacks/gcg/__init__.py +++ b/pyrit/auxiliary_attacks/gcg/__init__.py @@ -3,8 +3,8 @@ """Public API for the Greedy Coordinate Gradient (GCG) auxiliary attack. -The primary entry point is :class:`GCG` (alias for :class:`GCGGenerator`), a -:class:`pyrit.executor.promptgen.core.PromptGeneratorStrategy` that produces +The primary entry point is ``GCG`` (alias for ``GCGGenerator``), a +``pyrit.executor.promptgen.core.PromptGeneratorStrategy`` that produces adversarial suffixes via the GCG algorithm. Example: diff --git a/pyrit/auxiliary_attacks/gcg/config.py b/pyrit/auxiliary_attacks/gcg/config.py index cd5ee405e7..b18235131e 100644 --- a/pyrit/auxiliary_attacks/gcg/config.py +++ b/pyrit/auxiliary_attacks/gcg/config.py @@ -71,7 +71,7 @@ class GCGDataConfig: Used as a typed bundle for AML transport (a job ships its data config as a separate JSON file alongside the strategy ``GCGConfig``). Library callers loading goals/targets from a CSV can construct one and pass it to - :func:`pyrit.auxiliary_attacks.gcg.data.load_goals_and_targets`. + ``pyrit.auxiliary_attacks.gcg.data.load_goals_and_targets``. Attributes: train_data (str): URL or filesystem path to the training-data CSV. Empty @@ -100,7 +100,7 @@ def to_json(self) -> str: @classmethod def from_json(cls, payload: str) -> GCGDataConfig: - """Deserialize a config previously produced by :meth:`to_json`.""" + """Deserialize a config previously produced by ``to_json``.""" try: data = json.loads(payload) except json.JSONDecodeError as e: @@ -240,10 +240,10 @@ class GCGOutputConfig: class GCGConfig: """Top-level strategy configuration for one GCG attack run. - Bundles everything :class:`pyrit.auxiliary_attacks.gcg.GCGGenerator`'s + Bundles everything ``pyrit.auxiliary_attacks.gcg.GCGGenerator``'s constructor needs. Per-execution data (goals, targets) is **not** here — those flow through ``GCGGenerator.execute_async``, and for AML transport - they ride alongside this object as a separate :class:`GCGDataConfig` JSON. + they ride alongside this object as a separate ``GCGDataConfig`` JSON. Attributes: models (list[GCGModelConfig]): Training models the attack optimizes @@ -287,11 +287,11 @@ def to_json(self) -> str: @classmethod def from_json(cls, payload: str) -> GCGConfig: - """Deserialize a config previously produced by :meth:`to_json`. + """Deserialize a config previously produced by ``to_json``. Args: payload (str): JSON document matching the shape produced by - :meth:`to_json`. + ``to_json``. Returns: GCGConfig: A new ``GCGConfig`` reconstructed from ``payload``. @@ -308,7 +308,7 @@ def from_json(cls, payload: str) -> GCGConfig: @classmethod def from_json_file(cls, path: str | Path) -> GCGConfig: - """Load a config from a JSON file produced by :meth:`to_json_file`. + """Load a config from a JSON file produced by ``to_json_file``. Args: path (str | Path): Filesystem path to a JSON config file. diff --git a/pyrit/auxiliary_attacks/gcg/data.py b/pyrit/auxiliary_attacks/gcg/data.py index b9e8d32502..1c8b8efd3a 100644 --- a/pyrit/auxiliary_attacks/gcg/data.py +++ b/pyrit/auxiliary_attacks/gcg/data.py @@ -3,7 +3,7 @@ """CSV → goals/targets loader for the GCG attack. -Decoupled from :class:`GCGGenerator` so that callers with goals and targets +Decoupled from ``GCGGenerator`` so that callers with goals and targets already in memory can pass them straight into ``execute_async`` without going through ``pandas`` or any filesystem access. """ @@ -34,7 +34,7 @@ def load_goals_and_targets( ``train_data`` falls back to whatever default the legacy loader returns (an empty list today). random_seed (int): Seed used to shuffle the training rows. Defaults - to ``42`` to match :class:`GCGAlgorithmConfig`'s default. + to ``42`` to match ``GCGAlgorithmConfig``'s default. Returns: tuple[list[str], list[str], list[str], list[str]]: diff --git a/pyrit/auxiliary_attacks/gcg/experiments/run.py b/pyrit/auxiliary_attacks/gcg/experiments/run.py index 610531c1c9..396f46131d 100644 --- a/pyrit/auxiliary_attacks/gcg/experiments/run.py +++ b/pyrit/auxiliary_attacks/gcg/experiments/run.py @@ -1,10 +1,10 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. -"""Thin CLI wrapper around :meth:`GCGGenerator.execute_async` for AzureML jobs. +"""Thin CLI wrapper around ``GCGGenerator.execute_async`` for AzureML jobs. -The notebook (or any user) builds a :class:`GCGConfig` (strategy) and a -:class:`GCGDataConfig` (data) locally, serializes both with their respective +The notebook (or any user) builds a ``GCGConfig`` (strategy) and a +``GCGDataConfig`` (data) locally, serializes both with their respective ``to_json_file`` methods, ships them to Azure ML as job inputs, and the job's command line is:: @@ -14,7 +14,7 @@ --output-dir ${{outputs.results}} This file deserializes both configs inside the job, loads goals/targets from -the configured CSV, and runs the attack via a fresh :class:`GCGGenerator`. +the configured CSV, and runs the attack via a fresh ``GCGGenerator``. """ import argparse diff --git a/pyrit/models/conversation_reference.py b/pyrit/models/conversation_reference.py index 33d5e2d880..0915a045c4 100644 --- a/pyrit/models/conversation_reference.py +++ b/pyrit/models/conversation_reference.py @@ -57,7 +57,7 @@ def to_dict(self) -> dict[str, str | None]: Serialize to a JSON-compatible dictionary. .. deprecated:: - Use :meth:`model_dump` with ``mode="json"`` instead. This method + Use ``model_dump`` with ``mode="json"`` instead. This method will be removed in version 0.16.0. Returns: @@ -76,7 +76,7 @@ def from_dict(cls, data: dict[str, str | None]) -> ConversationReference: Reconstruct a ConversationReference from a dictionary. .. deprecated:: - Use :meth:`model_validate` instead. This method will be removed + Use ``model_validate`` instead. This method will be removed in version 0.16.0. Args: diff --git a/pyrit/models/retry_event.py b/pyrit/models/retry_event.py index 79bb2bbb6c..46a6e79fcf 100644 --- a/pyrit/models/retry_event.py +++ b/pyrit/models/retry_event.py @@ -38,7 +38,7 @@ def to_dict(self) -> dict: Serialize to a dictionary suitable for JSON storage. .. deprecated:: - Use :meth:`model_dump` with ``mode="json"`` instead. This method + Use ``model_dump`` with ``mode="json"`` instead. This method will be removed in version 0.16.0. Returns: @@ -57,7 +57,7 @@ def from_dict(cls, data: dict) -> RetryEvent: Deserialize from a dictionary. .. deprecated:: - Use :meth:`model_validate` instead. This method will be removed + Use ``model_validate`` instead. This method will be removed in version 0.16.0. Args: diff --git a/pyrit/setup/initializers/components/scenario_techniques.py b/pyrit/setup/initializers/components/scenario_techniques.py index 1506d67a69..5808c75ea4 100644 --- a/pyrit/setup/initializers/components/scenario_techniques.py +++ b/pyrit/setup/initializers/components/scenario_techniques.py @@ -5,9 +5,9 @@ Scenario technique initializer. This module owns the canonical catalog of scenario attack techniques as a -flat list of self-describing :class:`AttackTechniqueFactory` instances and -registers them into the singleton :class:`AttackTechniqueRegistry` via -:class:`ScenarioTechniqueInitializer`. +flat list of self-describing ``AttackTechniqueFactory`` instances and +registers them into the singleton ``AttackTechniqueRegistry`` via +``ScenarioTechniqueInitializer``. Per-name registration is idempotent: pre-existing entries in the registry are not overwritten. @@ -41,14 +41,14 @@ def build_scenario_technique_factories() -> list[AttackTechniqueFactory]: Factories that need an adversarial chat target do not bake one in; the default adversarial target is resolved lazily inside - :meth:`AttackTechniqueFactory.create` via + ``AttackTechniqueFactory.create`` via ``get_default_adversarial_target()``. Scenarios may also pass ``attack_adversarial_config_override`` at create time (but only when the factory did not bake one in at construction). A bare ``PromptSendingAttack`` factory is intentionally omitted from the catalog: every scenario whose ``BASELINE_ATTACK_POLICY`` is - :attr:`BaselineAttackPolicy.Enabled` already auto-prepends an equivalent + ``BaselineAttackPolicy.Enabled`` already auto-prepends an equivalent baseline atomic attack via ``Scenario._build_baseline_atomic_attack``. Returns: @@ -120,7 +120,7 @@ class ScenarioTechniqueInitializer(PyRITInitializer): ``Scenario._build_baseline_atomic_attack``) already covers that case. Registration is per-name idempotent: pre-existing entries in - :class:`AttackTechniqueRegistry` are not overwritten. + ``AttackTechniqueRegistry`` are not overwritten. """ async def initialize_async(self) -> None: From d2cdc0228510a75a1b9bd6aba82e616100b07a14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 22:00:41 +0000 Subject: [PATCH 3/3] Add API doc linking follow-ups in generator --- build_scripts/gen_api_md.py | 42 +++++++++- tests/unit/build_scripts/test_gen_api_md.py | 86 +++++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/build_scripts/gen_api_md.py b/build_scripts/gen_api_md.py index 41a52c3e76..9643040657 100644 --- a/build_scripts/gen_api_md.py +++ b/build_scripts/gen_api_md.py @@ -392,12 +392,11 @@ def render_class( """Render a class as markdown.""" name = cls["name"] bases = cls.get("bases", []) - bases_str = f"({', '.join(bases)})" if bases else "" anchor = _class_anchor(module, name) parts = [f"({anchor})=", f"## `{name}`\n"] - if bases_str: - parts.append(f"Bases: `{bases_str[1:-1]}`\n") + if bases: + parts.append(f"Bases: {_format_bases(bases, symbol_index)}\n") ds = cls.get("docstring", {}) text = _process_docstring_text(ds.get("text") if ds else None, symbol_index, current_class=name) @@ -443,6 +442,38 @@ def render_alias(alias: dict) -> str: return "\n".join(parts) +def _format_bases(bases: list[str], symbol_index: dict[str, list[SymbolEntry]] | None) -> str: + """Format class bases, linking resolvable symbols.""" + rendered_bases = [] + for base in bases: + rendered = f"`{base}`" + if symbol_index is not None: + rendered = _rewrite_symbol_refs(rendered, symbol_index) + rendered_bases.append(rendered) + return ", ".join(rendered_bases) + + +def _format_reexport_alias(mod_name: str, alias_name: str, symbol_index: dict[str, list[SymbolEntry]] | None) -> str: + """Format a re-export alias name, preferring module-qualified lookup.""" + rendered = f"`{alias_name}`" + if symbol_index is None: + return rendered + + module_qualified = _resolve_symbol(f"{mod_name}.{alias_name}", symbol_index, current_class=None) + if module_qualified is not None: + return f"[{rendered}](#{module_qualified.anchor})" + + return _rewrite_symbol_refs(rendered, symbol_index) + + +def _format_reexport_target(target: str, symbol_index: dict[str, list[SymbolEntry]] | None) -> str: + """Format a re-export target, linking resolvable symbols.""" + rendered = f"`{target}`" + if symbol_index is None: + return rendered + return _rewrite_symbol_refs(rendered, symbol_index) + + def render_module( data: dict, *, @@ -454,6 +485,7 @@ def render_module( parts = [ "---", f"short_title: {short_name}", + f"label: api-{_module_slug(mod_name)}", "---\n", f"# {mod_name}\n", ] @@ -479,7 +511,9 @@ def render_module( parts.append("## Re-exports\n") for a in aliases: target = a.get("target", "") - parts.append(f"- `{a['name']}` → `{target}`\n") + alias_md = _format_reexport_alias(mod_name, a["name"], symbol_index) + target_md = _format_reexport_target(target, symbol_index) + parts.append(f"- {alias_md} → {target_md}\n") return "\n".join(parts) diff --git a/tests/unit/build_scripts/test_gen_api_md.py b/tests/unit/build_scripts/test_gen_api_md.py index 5eca97161a..8293ec8955 100644 --- a/tests/unit/build_scripts/test_gen_api_md.py +++ b/tests/unit/build_scripts/test_gen_api_md.py @@ -9,7 +9,9 @@ _method_anchor, _process_docstring_text, _rewrite_symbol_refs, + render_class, render_function, + render_module, ) @@ -348,3 +350,87 @@ def test_render_function_uses_method_anchor_when_class_name_given() -> None: assert "(api-pyrit_prompt_target-PromptTarget-validate)=" in out assert "#### `validate`" in out assert "[``send_prompt_async``](#api-pyrit_prompt_target-PromptTarget-send_prompt_async)" in out + + +def test_render_class_links_known_base_but_keeps_external_bases_plain() -> None: + cls = { + "name": "AzureBlobStorageTarget", + "kind": "class", + "bases": ["PromptTarget", "str", "Enum"], + } + index = { + "PromptTarget": [ + SymbolEntry( + module="pyrit.prompt_target", + kind="class", + name="PromptTarget", + qualname="PromptTarget", + anchor="api-pyrit_prompt_target-PromptTarget", + ) + ] + } + + out = render_class(cls, module="pyrit.prompt_target.azure_blob_storage_target", symbol_index=index) + assert "Bases: [`PromptTarget`](#api-pyrit_prompt_target-PromptTarget), `str`, `Enum`" in out + + +def test_render_module_emits_page_label_frontmatter() -> None: + data = {"name": "pyrit.prompt_target", "members": []} + out = render_module(data, symbol_index={}) + + assert out.startswith("---\nshort_title: prompt_target\nlabel: api-pyrit_prompt_target\n---") + + +def test_render_module_reexports_prefer_module_qualified_alias_and_link_targets() -> None: + data = { + "name": "pyrit.prompt_target", + "members": [ + { + "name": "PromptTargetAlias", + "kind": "alias", + "target": "pyrit.prompt_target.PromptTarget", + } + ], + } + alias_entry = SymbolEntry( + module="pyrit.prompt_target", + kind="class", + name="PromptTargetAlias", + qualname="PromptTargetAlias", + anchor="api-pyrit_prompt_target-PromptTargetAlias", + ) + target_entry = SymbolEntry( + module="pyrit.prompt_target", + kind="class", + name="PromptTarget", + qualname="PromptTarget", + anchor="api-pyrit_prompt_target-PromptTarget", + ) + index = { + "PromptTargetAlias": [alias_entry], + "pyrit.prompt_target.PromptTargetAlias": [alias_entry], + "PromptTarget": [target_entry], + "pyrit.prompt_target.PromptTarget": [target_entry], + } + + out = render_module(data, symbol_index=index) + assert "- [`PromptTargetAlias`](#api-pyrit_prompt_target-PromptTargetAlias) → " in out + assert "[`pyrit.prompt_target.PromptTarget`](#api-pyrit_prompt_target-PromptTarget)" in out + + +def test_render_module_reexports_fall_back_to_short_name_alias_lookup() -> None: + data = { + "name": "pyrit.prompt_target", + "members": [{"name": "PromptTargetAlias", "kind": "alias", "target": "external.Type"}], + } + alias_entry = SymbolEntry( + module="pyrit.aliases", + kind="class", + name="PromptTargetAlias", + qualname="PromptTargetAlias", + anchor="api-pyrit_aliases-PromptTargetAlias", + ) + index = {"PromptTargetAlias": [alias_entry]} + + out = render_module(data, symbol_index=index) + assert "- [`PromptTargetAlias`](#api-pyrit_aliases-PromptTargetAlias) → `external.Type`" in out