Skip to content

Commit bf95fcb

Browse files
romanlutzCopilot
andauthored
DOC: Auto-link symbol references in generated API docs (#1823)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3253c1e commit bf95fcb

10 files changed

Lines changed: 823 additions & 50 deletions

File tree

.github/instructions/style-guide.instructions.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -196,28 +196,43 @@ def calculate_score(
196196

197197
The PyRIT docs build uses **MyST** (Markdown-flavoured), not reStructuredText.
198198
Do **not** use reST cross-reference roles in docstrings or module comments —
199-
they render as raw text under MyST and are inconsistent with the rest of the
200-
codebase, which uses plain double-backtick code spans for symbol names.
199+
they render as raw text under MyST. A pre-commit hook
200+
(`check_no_rest_roles`) blocks new ones from landing.
201+
202+
Use plain double-backticks for symbol references. The API page generator
203+
(`build_scripts/gen_api_md.py`) automatically rewrites known PyRIT symbol
204+
names into MyST cross-reference links at build time, so you get clickable
205+
navigation in the rendered docs without any extra markup.
201206

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

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

214-
Roles to avoid include `:class:`, `:func:`, `:meth:`, `:mod:`, `:attr:`,
215-
`:data:`, `:exc:`, `:obj:`, `:ref:`, and any `:py:*:` variants
216-
(e.g. `:py:class:`, `:py:func:`).
220+
The auto-linker resolves:
221+
222+
- bare class/function names (`` ``SeedPrompt`` ``)
223+
- `Class.method` references (`` ``PromptTarget.apply_capabilities`` ``)
224+
- fully-qualified paths (`` ``pyrit.models.SeedPrompt`` ``)
225+
- bare method names when the docstring is on the owning class
226+
(`` ``send_prompt_async`` `` inside `PromptTarget`)
227+
228+
Ambiguous short names (e.g. two unrelated classes both called `Scorer`)
229+
are left as plain code-spans; spell out the FQN when you need a stable
230+
cross-reference. Unknown names also stay as plain code-spans, so
231+
docstrings remain safe to write without consulting the symbol index.
217232

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

222237
### Class-Level Constants
223238
- Define constants as class attributes, not module-level

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ repos:
3030
files: ^(doc/.*\.(py|ipynb|md)|doc/myst\.yml)$
3131
pass_filenames: false
3232
additional_dependencies: ['pyyaml']
33+
- id: check-no-rest-roles
34+
name: Reject Sphinx reST cross-reference roles
35+
entry: python ./build_scripts/check_no_rest_roles.py
36+
language: python
37+
files: ^pyrit/.*\.py$
3338
- id: enforce_alembic_revision_immutability
3439
name: Enforce Alembic Revision Immutability
3540
entry: python ./build_scripts/enforce_alembic_revision_immutability.py
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT license.
3+
4+
"""Pre-commit guard against Sphinx reST cross-reference roles in source.
5+
6+
PyRIT docs render docstrings through MyST (jupyter-book 2), not Sphinx, so
7+
reST roles like ``:class:`Foo``` show up as raw literal text in the built
8+
site. The standing convention (style-guide.instructions.md) is to
9+
use plain double-backticks; ``build_scripts/gen_api_md.py`` then auto-links
10+
known PyRIT symbols at render time.
11+
12+
This hook flags any newly introduced reST role inside ``pyrit/`` so it can
13+
be replaced before landing. Run it manually with::
14+
15+
uv run python build_scripts/check_no_rest_roles.py
16+
17+
or rely on the ``check-no-rest-roles`` pre-commit hook in
18+
``.pre-commit-config.yaml``.
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import re
24+
import sys
25+
from pathlib import Path
26+
27+
# Roles flagged by this guard. Mirrors the list in the style guide. The
28+
# pattern matches the leading colon, role name, and the opening backtick of
29+
# the role argument (e.g. ``:class:`Foo```), so backticked code spans that
30+
# happen to start with a colon character are not caught.
31+
_REST_ROLE_RE = re.compile(r":(?:class|func|meth|mod|attr|data|exc|obj|ref|py:[a-z]+):`")
32+
33+
34+
def _check_file(path: Path) -> list[tuple[int, str]]:
35+
findings: list[tuple[int, str]] = []
36+
try:
37+
text = path.read_text(encoding="utf-8")
38+
except (OSError, UnicodeDecodeError):
39+
return findings
40+
for lineno, line in enumerate(text.splitlines(), start=1):
41+
if _REST_ROLE_RE.search(line):
42+
findings.append((lineno, line.rstrip()))
43+
return findings
44+
45+
46+
def main(argv: list[str] | None = None) -> int:
47+
args = list(argv if argv is not None else sys.argv[1:])
48+
if not args:
49+
return 0
50+
51+
failures: list[tuple[Path, list[tuple[int, str]]]] = []
52+
for raw in args:
53+
path = Path(raw)
54+
if path.suffix != ".py":
55+
continue
56+
findings = _check_file(path)
57+
if findings:
58+
failures.append((path, findings))
59+
60+
if not failures:
61+
return 0
62+
63+
print("\nreST cross-reference roles are not allowed in PyRIT source.")
64+
print("PyRIT renders docstrings with MyST, not Sphinx — these roles show")
65+
print("up as raw literal text in the built docs.\n")
66+
print("Replace ``:class:`Foo``` / ``:func:`bar``` / ``:meth:`Baz.do``` etc.")
67+
print("with plain double-backticks (``Foo``). build_scripts/gen_api_md.py")
68+
print("auto-links known PyRIT symbols at render time.\n")
69+
for path, findings in failures:
70+
for lineno, snippet in findings:
71+
print(f" {path}:{lineno}: {snippet}")
72+
print()
73+
return 1
74+
75+
76+
if __name__ == "__main__":
77+
raise SystemExit(main())

0 commit comments

Comments
 (0)