Skip to content

[DRAFT] DOC Add API example backlinks - #2282

Draft
hugosmoreira wants to merge 2 commits into
microsoft:mainfrom
hugosmoreira:docs/api-example-backlinks
Draft

[DRAFT] DOC Add API example backlinks#2282
hugosmoreira wants to merge 2 commits into
microsoft:mainfrom
hugosmoreira:docs/api-example-backlinks

Conversation

@hugosmoreira

Copy link
Copy Markdown

Description

Closes #654.

PyRIT's documentation has moved from the Sphinx-based setup described in the original issue to Jupyter Book 2/MyST and the custom gen_api_md.py generator. This updates that generator to build API-to-guide backlinks automatically:

  • scan only published user-guide pages from doc/myst.yml
  • parse Jupytext companions, standalone notebooks, and Markdown Python fences without executing documentation code
  • link classes and top-level functions only when they are imported and actually referenced
  • handle aliases, module imports, deep implementation imports, ambiguity, deduplication, and deterministic ordering
  • render an Examples list on the matching API class/function section

Jupytext .py companions are used as the analysis source while the generated links continue to point to their .ipynb pages.

The analysis is intentionally conservative: star/dynamic imports and instance-method type inference are not guessed. Invalid illustrative snippets are skipped so they cannot break API generation.

Tests and Documentation

  • python -m pytest tests/unit/build_scripts/test_gen_api_md.py -q --confcutdir=tests/unit/build_scripts — 39 passed
  • ruff check build_scripts/gen_api_md.py tests/unit/build_scripts/test_gen_api_md.py
  • ruff format --check build_scripts/gen_api_md.py tests/unit/build_scripts/test_gen_api_md.py
  • ty check build_scripts/gen_api_md.py tests/unit/build_scripts/test_gen_api_md.py
  • generated the real API reference with pydoc2json.py and gen_api_md.py
  • validated all 157 myst.yml references
  • verified all 668 generated example links resolve to existing local documentation pages
  • reran generation and confirmed deterministic output

No documentation source notebooks changed, so Jupytext execution was not applicable.

Disclosure: I used Codex as a coding assistant while implementing and validating this change. I reviewed the patch and generated output and will own any follow-up changes from review.

@hugosmoreira

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

@romanlutz romanlutz self-assigned this Aug 2, 2026
return paths


def _build_example_index(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of new responsibility for gen_api_md.py. The module docstring says its job is "Reads the JSON files produced by pydoc2json.py and generates clean MyST markdown pages" — this PR adds a second, unrelated subsystem: TOC YAML parsing, .ipynb JSON parsing, Jupytext percent-format splitting, Markdown fence extraction, frontmatter/H1 title extraction, and Python import/AST resolution. That's ~250 lines with their own failure modes, and it roughly doubles the file's non-rendering logic.

Suggest extracting everything from _unique_public_symbol through _build_example_index into build_scripts/example_index.py and importing it, the same way validate_docs is already imported as a sibling. gen_api_md.py then keeps one job: JSON → markdown. Fine as a follow-up if you'd rather not grow this PR.

parts = ["**Examples:**\n"]
for example in examples:
title = example.title.replace("[", r"\[").replace("]", r"\]")
parts.append(f"- [{title}](../{example.path})")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ../ prefix hardcodes the assumption that API pages sit exactly one directory below the doc root. True today (API_MD_DIR = Path("doc/api")), but the coupling is invisible from here and would break silently — MyST only surfaces unresolved links under --strict, which the docs workflow deliberately doesn't use. Worth deriving the prefix from API_MD_DIR relative to the doc root instead.

Comment on lines +1016 to +1020
example_index = _build_example_index(
doc_root=Path("doc"),
toc_path=Path("doc/myst.yml"),
symbol_index=symbol_index,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three independent silent-skip paths stack up behind this call: SyntaxError → skip chunk, JSONDecodeError/OSError → skip notebook, not path.is_file() → skip TOC entry. If notebook generation order changes in CI, or a TOC refactor moves pages under a newly-excluded prefix, the index quietly empties and the build still passes green.

main() already prints per-module counts below; a matching one-liner here — e.g. Indexed examples: {len(example_index)} symbols across {n} pages — would make a regression visible in CI logs. Cheap insurance.

module=module,
class_name=name,
symbol_index=symbol_index,
examples_by_anchor=examples_by_anchor,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter is dead on the method path. _unique_public_symbol filters to kind in ("class", "function"), so _build_example_index can only key on class and function anchors — method anchors from _method_anchor are structurally distinct and will never match. I confirmed empirically: 0 method-level **Examples:** blocks across all generated pages.

Either drop it here so the contract is honest, or make it real. Method-level backlinks aren't derivable from the current design anyway (attack.execute_async(...) roots on a local variable, not an import binding), so removing the plumbing seems like the cleaner call.

Comment on lines +272 to +274
short_entries = symbol_index.get(qualified_name.rsplit(".", 1)[-1], [])
module_matches = [entry for entry in short_entries if qualified_name.startswith(f"{entry.module}.")]
return _unique_public_symbol(module_matches) or _unique_public_symbol(short_entries)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a bug — I probed all 669 generated (symbol, page) pairs and found zero where the symbol name is absent from the page source, so no false positives on the current corpus. But this fallback and the segment[:1].isupper() branch in _resolve_attribute_symbol both discard the declared module and rely on API-wide name uniqueness. That leniency is exactly what makes re-export imports (from pyrit.prompt_target.openai.openai_chat_target import ...) resolve, so it should stay — it just reads like an oversight. A one-line comment stating the invariant (same assumption _resolve_symbol already makes) would save the next reader a detour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DOC cross-linking from API reference to user guide

2 participants