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
35 changes: 25 additions & 10 deletions .github/instructions/style-guide.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,28 +196,43 @@ def calculate_score(

The PyRIT docs build uses **MyST** (Markdown-flavoured), not reStructuredText.
Do **not** use reST cross-reference roles in docstrings or module comments —
they render as raw text under MyST and are inconsistent with the rest of the
codebase, which uses plain double-backtick code spans for symbol names.
they render as raw text under MyST. A pre-commit hook
(`check_no_rest_roles`) blocks new ones from landing.

Use plain double-backticks for symbol references. The API page generator
(`build_scripts/gen_api_md.py`) automatically rewrites known PyRIT symbol
names into MyST cross-reference links at build time, so you get clickable
navigation in the rendered docs without any extra markup.

```python
# WRONG — reST roles render as literal `:class:\`SeedPrompt\`` under MyST
# WRONG — reST roles render as literal `:class:\`SeedPrompt\`` under MyST,
# and the pre-commit guard will reject them
"""Returns a :class:`SeedPrompt` instance."""
"""Delegate to :func:`download_files_async` (deprecated alias)."""
"""See :meth:`PromptTarget.apply_capabilities` for details."""

# CORRECT — plain double-backtick code span (matches existing convention)
# CORRECT — plain double-backticks; gen_api_md.py auto-links these
"""Returns a ``SeedPrompt`` instance."""
"""Delegate to ``download_files_async`` (deprecated alias)."""
"""See ``PromptTarget.apply_capabilities`` for details."""
```

Roles to avoid include `:class:`, `:func:`, `:meth:`, `:mod:`, `:attr:`,
`:data:`, `:exc:`, `:obj:`, `:ref:`, and any `:py:*:` variants
(e.g. `:py:class:`, `:py:func:`).
The auto-linker resolves:

- bare class/function names (`` ``SeedPrompt`` ``)
- `Class.method` references (`` ``PromptTarget.apply_capabilities`` ``)
- fully-qualified paths (`` ``pyrit.models.SeedPrompt`` ``)
- bare method names when the docstring is on the owning class
(`` ``send_prompt_async`` `` inside `PromptTarget`)

Ambiguous short names (e.g. two unrelated classes both called `Scorer`)
are left as plain code-spans; spell out the FQN when you need a stable
cross-reference. Unknown names also stay as plain code-spans, so
docstrings remain safe to write without consulting the symbol index.

If you genuinely need a Sphinx cross-reference (rare in PyRIT — most
docstrings just name the symbol in backticks), use the MyST role syntax
`` {class}`Name` `` instead. The default, though, is plain double-backticks.
If you need an explicit MyST link in markdown documentation, use the
standard syntax `` [`Name`](#api-pyrit_module-Name) `` — but inside
Python docstrings this should be rare; plain backticks are the default.

### Class-Level Constants
- Define constants as class attributes, not module-level
Expand Down
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ repos:
files: ^(doc/.*\.(py|ipynb|md)|doc/myst\.yml)$
pass_filenames: false
additional_dependencies: ['pyyaml']
- id: check-no-rest-roles
name: Reject Sphinx reST cross-reference roles
entry: python ./build_scripts/check_no_rest_roles.py
language: python
files: ^pyrit/.*\.py$
- id: enforce_alembic_revision_immutability
name: Enforce Alembic Revision Immutability
entry: python ./build_scripts/enforce_alembic_revision_immutability.py
Expand Down
77 changes: 77 additions & 0 deletions build_scripts/check_no_rest_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.

"""Pre-commit guard against Sphinx reST cross-reference roles in source.

PyRIT docs render docstrings through MyST (jupyter-book 2), not Sphinx, so
reST roles like ``:class:`Foo``` show up as raw literal text in the built
site. The standing convention (style-guide.instructions.md, PR #1782) is to
use plain double-backticks; ``build_scripts/gen_api_md.py`` then auto-links
known PyRIT symbols at render time.

This hook flags any newly introduced reST role inside ``pyrit/`` so it can
be replaced before landing. Run it manually with::

uv run python build_scripts/check_no_rest_roles.py

or rely on the ``check-no-rest-roles`` pre-commit hook in
``.pre-commit-config.yaml``.
"""

from __future__ import annotations

import re
import sys
from pathlib import Path

# Roles flagged by this guard. Mirrors the list in the style guide. The
# pattern matches the leading colon, role name, and the opening backtick of
# the role argument (e.g. ``:class:`Foo```), so backticked code spans that
# happen to start with a colon character are not caught.
_REST_ROLE_RE = re.compile(r":(?:class|func|meth|mod|attr|data|exc|obj|ref|py:[a-z]+):`")


def _check_file(path: Path) -> list[tuple[int, str]]:
findings: list[tuple[int, str]] = []
try:
text = path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return findings
for lineno, line in enumerate(text.splitlines(), start=1):
if _REST_ROLE_RE.search(line):
findings.append((lineno, line.rstrip()))
return findings


def main(argv: list[str] | None = None) -> int:
args = list(argv if argv is not None else sys.argv[1:])
if not args:
return 0

failures: list[tuple[Path, list[tuple[int, str]]]] = []
for raw in args:
path = Path(raw)
if path.suffix != ".py":
continue
findings = _check_file(path)
if findings:
failures.append((path, findings))

if not failures:
return 0

print("\nreST cross-reference roles are not allowed in PyRIT source.")
print("PyRIT renders docstrings with MyST, not Sphinx — these roles show")
print("up as raw literal text in the built docs.\n")
print("Replace ``:class:`Foo``` / ``:func:`bar``` / ``:meth:`Baz.do``` etc.")
print("with plain double-backticks (``Foo``). build_scripts/gen_api_md.py")
print("auto-links known PyRIT symbols at render time.\n")
for path, findings in failures:
for lineno, snippet in findings:
print(f" {path}:{lineno}: {snippet}")
print()
return 1


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading