Add sphinx-gp-opengraph + sphinx-gp-sitemap; register-aware autodoc-docutils discovery#22
Add sphinx-gp-opengraph + sphinx-gp-sitemap; register-aware autodoc-docutils discovery#22
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #22 +/- ##
==========================================
+ Coverage 89.89% 90.20% +0.30%
==========================================
Files 148 163 +15
Lines 13134 13876 +742
==========================================
+ Hits 11807 12517 +710
- Misses 1327 1359 +32 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Code reviewFound 2 issues:
gp-sphinx/packages/gp-opengraph/src/gp_opengraph/__init__.py Lines 39 to 42 in 6c31af6 gp-sphinx/packages/gp-sitemap/src/gp_sitemap/__init__.py Lines 46 to 49 in 6c31af6
gp-sphinx/packages/gp-opengraph/src/gp_opengraph/__init__.py Lines 260 to 267 in 6c31af6 gp-sphinx/packages/gp-sitemap/src/gp_sitemap/__init__.py Lines 228 to 235 in 6c31af6 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…iling periods why: PR #22 code review flagged two CLAUDE.md#logging violations in the two new packages. These are house-style invariants that every existing workspace package (gp_sphinx, sphinx_fonts, sphinx_ux_badges, …) already follows: 1. "Add ``NullHandler`` in library ``__init__.py`` files" (CLAUDE.md line 553) — both new package __init__.py files declared a module-level logger without one. Importing either package outside a Sphinx build could surface a "no handlers could be found" warning from stdlib logging. 2. "No trailing punctuation" in log messages (CLAUDE.md line 576) — two log calls introduced in commit ab3a6a6 and 04f0121 ended with a period. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * After ``logger = logging.getLogger(__name__)`` attach ``logger.addHandler(logging.NullHandler())`` to match the sphinx-fonts pattern exactly. * ``_warn_if_social_cards_used``: message reworded to "ogp_social_cards ignored — gp-opengraph ships no card generator; use a static PNG via ogp_image (site default) or per-page 'og:image' frontmatter" (no trailing period; also trimmed the "is ignored — gp-opengraph does not bundle a card generator" phrasing into "ignored — … ships no card generator" which reads cleaner as a warning). - packages/gp-sitemap/src/gp_sitemap/__init__.py: * Import ``logging`` from stdlib and add ``logging.getLogger(__name__).addHandler(logging.NullHandler())`` alongside the existing ``logger = getLogger(__name__)`` from ``sphinx.util.logging``. The Sphinx adapter is kept because it supports the ``type=``/``subtype=`` kwargs used elsewhere for warning classification; the NullHandler attaches to the underlying stdlib logger with the same name so the library is well-behaved when imported outside Sphinx. Inline comment memorializes the two-getLogger dance so future readers don't try to delete one. * ``_write_sitemap`` "skipping sitemap" info message: trailing period removed. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0: 1199 passed, 3 skipped - just build-docs: build succeeded
…ules why: PR #22 code review flagged three ``from <stdlib> import <name>`` import statements that violate CLAUDE.md line 457: Use namespace imports for standard library modules: `import enum` instead of `from enum import Enum` Exception: `dataclasses` module may use `from dataclasses import dataclass, field` for cleaner decorator syntax Every existing workspace package follows this — e.g. gp-sphinx's own config.py uses ``import pathlib`` / ``import tomllib`` then ``pathlib.Path(...)`` / ``tomllib.loads(...)``. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * ``from types import NoneType`` -> ``import types`` + ``types.NoneType`` at each call site (four occurrences inside ``frozenset({...})`` literals for add_config_value). * ``from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit`` -> ``import urllib.parse`` with each call qualified: ``urllib.parse.urljoin(...)``, ``urllib.parse.urlparse(...)``, ``urllib.parse.urlsplit(...)``, ``urllib.parse.urlunsplit(...)``. - packages/gp-opengraph/src/gp_opengraph/_title.py: * ``from html.parser import HTMLParser`` -> ``import html.parser``; subclass declaration becomes ``class HTMLTextParser(html.parser.HTMLParser)``. - packages/gp-opengraph/src/gp_opengraph/_meta.py: * Same HTMLParser rewrite as _title.py. Left as-is (already compliant): - ``from xml.etree import ElementTree`` in gp_sitemap — this imports a *submodule*, not a name, and is the canonical idiom (equivalent to ``import xml.etree.ElementTree as ElementTree``). CLAUDE.md's example targets ``from enum import Enum`` (name import), not submodule aliasing. - ``from collections.abc import Set`` / ``Iterable`` inside ``TYPE_CHECKING`` blocks — gp_sphinx itself does this at config.py#L17, so the convention allows type-only imports this way. No behavior change; purely an import refactor. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0 -vvv: 1199 passed, 3 skipped - just build-docs: build succeeded
…iling periods why: PR #22 code review flagged two CLAUDE.md#logging violations in the two new packages. These are house-style invariants that every existing workspace package (gp_sphinx, sphinx_fonts, sphinx_ux_badges, …) already follows: 1. "Add ``NullHandler`` in library ``__init__.py`` files" (CLAUDE.md line 553) — both new package __init__.py files declared a module-level logger without one. Importing either package outside a Sphinx build could surface a "no handlers could be found" warning from stdlib logging. 2. "No trailing punctuation" in log messages (CLAUDE.md line 576) — two log calls introduced in commit ab3a6a6 and 04f0121 ended with a period. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * After ``logger = logging.getLogger(__name__)`` attach ``logger.addHandler(logging.NullHandler())`` to match the sphinx-fonts pattern exactly. * ``_warn_if_social_cards_used``: message reworded to "ogp_social_cards ignored — gp-opengraph ships no card generator; use a static PNG via ogp_image (site default) or per-page 'og:image' frontmatter" (no trailing period; also trimmed the "is ignored — gp-opengraph does not bundle a card generator" phrasing into "ignored — … ships no card generator" which reads cleaner as a warning). - packages/gp-sitemap/src/gp_sitemap/__init__.py: * Import ``logging`` from stdlib and add ``logging.getLogger(__name__).addHandler(logging.NullHandler())`` alongside the existing ``logger = getLogger(__name__)`` from ``sphinx.util.logging``. The Sphinx adapter is kept because it supports the ``type=``/``subtype=`` kwargs used elsewhere for warning classification; the NullHandler attaches to the underlying stdlib logger with the same name so the library is well-behaved when imported outside Sphinx. Inline comment memorializes the two-getLogger dance so future readers don't try to delete one. * ``_write_sitemap`` "skipping sitemap" info message: trailing period removed. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0: 1199 passed, 3 skipped - just build-docs: build succeeded
…ules why: PR #22 code review flagged three ``from <stdlib> import <name>`` import statements that violate CLAUDE.md line 457: Use namespace imports for standard library modules: `import enum` instead of `from enum import Enum` Exception: `dataclasses` module may use `from dataclasses import dataclass, field` for cleaner decorator syntax Every existing workspace package follows this — e.g. gp-sphinx's own config.py uses ``import pathlib`` / ``import tomllib`` then ``pathlib.Path(...)`` / ``tomllib.loads(...)``. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * ``from types import NoneType`` -> ``import types`` + ``types.NoneType`` at each call site (four occurrences inside ``frozenset({...})`` literals for add_config_value). * ``from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit`` -> ``import urllib.parse`` with each call qualified: ``urllib.parse.urljoin(...)``, ``urllib.parse.urlparse(...)``, ``urllib.parse.urlsplit(...)``, ``urllib.parse.urlunsplit(...)``. - packages/gp-opengraph/src/gp_opengraph/_title.py: * ``from html.parser import HTMLParser`` -> ``import html.parser``; subclass declaration becomes ``class HTMLTextParser(html.parser.HTMLParser)``. - packages/gp-opengraph/src/gp_opengraph/_meta.py: * Same HTMLParser rewrite as _title.py. Left as-is (already compliant): - ``from xml.etree import ElementTree`` in gp_sitemap — this imports a *submodule*, not a name, and is the canonical idiom (equivalent to ``import xml.etree.ElementTree as ElementTree``). CLAUDE.md's example targets ``from enum import Enum`` (name import), not submodule aliasing. - ``from collections.abc import Set`` / ``Iterable`` inside ``TYPE_CHECKING`` blocks — gp_sphinx itself does this at config.py#L17, so the convention allows type-only imports this way. No behavior change; purely an import refactor. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0 -vvv: 1199 passed, 3 skipped - just build-docs: build succeeded
35d7c64 to
b5138cd
Compare
…ubsections
why: the existing CHANGES Features bullets for gp-opengraph and
gp-sitemap had grown into multi-paragraph mini-READMEs, and they
predated both the package rename to the sphinx-gp-* prefix and the
flat sitemap_url_scheme = "{link}" auto-set. Replace them with three
brief #### subsections that describe the shipped surface in two
sentences each, name the renamed packages, and carry the (#22) PR
ref.
what:
- CHANGES: replace the three SEO Features bullets with three ####
subsections — "New package: sphinx-gp-opengraph", "New package:
sphinx-gp-sitemap", and "gp-sphinx: SEO config auto-wired from
docs_url" — each ~3 lines, with (#22) PR refs
…iling periods why: PR #22 code review flagged two CLAUDE.md#logging violations in the two new packages. These are house-style invariants that every existing workspace package (gp_sphinx, sphinx_fonts, sphinx_ux_badges, …) already follows: 1. "Add ``NullHandler`` in library ``__init__.py`` files" (CLAUDE.md line 553) — both new package __init__.py files declared a module-level logger without one. Importing either package outside a Sphinx build could surface a "no handlers could be found" warning from stdlib logging. 2. "No trailing punctuation" in log messages (CLAUDE.md line 576) — two log calls introduced in commit ab3a6a6 and 04f0121 ended with a period. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * After ``logger = logging.getLogger(__name__)`` attach ``logger.addHandler(logging.NullHandler())`` to match the sphinx-fonts pattern exactly. * ``_warn_if_social_cards_used``: message reworded to "ogp_social_cards ignored — gp-opengraph ships no card generator; use a static PNG via ogp_image (site default) or per-page 'og:image' frontmatter" (no trailing period; also trimmed the "is ignored — gp-opengraph does not bundle a card generator" phrasing into "ignored — … ships no card generator" which reads cleaner as a warning). - packages/gp-sitemap/src/gp_sitemap/__init__.py: * Import ``logging`` from stdlib and add ``logging.getLogger(__name__).addHandler(logging.NullHandler())`` alongside the existing ``logger = getLogger(__name__)`` from ``sphinx.util.logging``. The Sphinx adapter is kept because it supports the ``type=``/``subtype=`` kwargs used elsewhere for warning classification; the NullHandler attaches to the underlying stdlib logger with the same name so the library is well-behaved when imported outside Sphinx. Inline comment memorializes the two-getLogger dance so future readers don't try to delete one. * ``_write_sitemap`` "skipping sitemap" info message: trailing period removed. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0: 1199 passed, 3 skipped - just build-docs: build succeeded
…ules why: PR #22 code review flagged three ``from <stdlib> import <name>`` import statements that violate CLAUDE.md line 457: Use namespace imports for standard library modules: `import enum` instead of `from enum import Enum` Exception: `dataclasses` module may use `from dataclasses import dataclass, field` for cleaner decorator syntax Every existing workspace package follows this — e.g. gp-sphinx's own config.py uses ``import pathlib`` / ``import tomllib`` then ``pathlib.Path(...)`` / ``tomllib.loads(...)``. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * ``from types import NoneType`` -> ``import types`` + ``types.NoneType`` at each call site (four occurrences inside ``frozenset({...})`` literals for add_config_value). * ``from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit`` -> ``import urllib.parse`` with each call qualified: ``urllib.parse.urljoin(...)``, ``urllib.parse.urlparse(...)``, ``urllib.parse.urlsplit(...)``, ``urllib.parse.urlunsplit(...)``. - packages/gp-opengraph/src/gp_opengraph/_title.py: * ``from html.parser import HTMLParser`` -> ``import html.parser``; subclass declaration becomes ``class HTMLTextParser(html.parser.HTMLParser)``. - packages/gp-opengraph/src/gp_opengraph/_meta.py: * Same HTMLParser rewrite as _title.py. Left as-is (already compliant): - ``from xml.etree import ElementTree`` in gp_sitemap — this imports a *submodule*, not a name, and is the canonical idiom (equivalent to ``import xml.etree.ElementTree as ElementTree``). CLAUDE.md's example targets ``from enum import Enum`` (name import), not submodule aliasing. - ``from collections.abc import Set`` / ``Iterable`` inside ``TYPE_CHECKING`` blocks — gp_sphinx itself does this at config.py#L17, so the convention allows type-only imports this way. No behavior change; purely an import refactor. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0 -vvv: 1199 passed, 3 skipped - just build-docs: build succeeded
…ubsections why: after the filter-repo sweep collapsed the gp-* → sphinx-gp-* rename across the entire branch history, the Features section had three multi-paragraph bullets describing the new packages plus the gp-sphinx auto-wiring. Replace them with three brief #### subsections that describe the shipped surface in two sentences each, name the renamed packages, and carry the (#22) PR ref. what: - CHANGES: replace the three SEO Features bullets with three #### subsections — "New package: sphinx-gp-opengraph", "New package: sphinx-gp-sitemap", and "gp-sphinx: SEO config auto-wired from docs_url" — each ~3 lines, with (#22) PR refs - uv.lock: refresh certifi (incidental)
…ubsections why: after the filter-repo sweep collapsed the gp-* → sphinx-gp-* rename across the entire branch history, the Features section had three multi-paragraph bullets describing the new packages plus the gp-sphinx auto-wiring. Replace them with three brief #### subsections that describe the shipped surface in two sentences each, name the renamed packages, and carry the (#22) PR ref. what: - CHANGES: replace the three SEO Features bullets with three #### subsections — "New package: sphinx-gp-opengraph", "New package: sphinx-gp-sitemap", and "gp-sphinx: SEO config auto-wired from docs_url" — each ~3 lines, with (#22) PR refs
…iling periods why: PR #22 code review flagged two CLAUDE.md#logging violations in the two new packages. These are house-style invariants that every existing workspace package (gp_sphinx, sphinx_fonts, sphinx_ux_badges, …) already follows: 1. "Add ``NullHandler`` in library ``__init__.py`` files" (CLAUDE.md line 553) — both new package __init__.py files declared a module-level logger without one. Importing either package outside a Sphinx build could surface a "no handlers could be found" warning from stdlib logging. 2. "No trailing punctuation" in log messages (CLAUDE.md line 576) — two log calls introduced in commit ab3a6a6 and 04f0121 ended with a period. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * After ``logger = logging.getLogger(__name__)`` attach ``logger.addHandler(logging.NullHandler())`` to match the sphinx-fonts pattern exactly. * ``_warn_if_social_cards_used``: message reworded to "ogp_social_cards ignored — gp-opengraph ships no card generator; use a static PNG via ogp_image (site default) or per-page 'og:image' frontmatter" (no trailing period; also trimmed the "is ignored — gp-opengraph does not bundle a card generator" phrasing into "ignored — … ships no card generator" which reads cleaner as a warning). - packages/gp-sitemap/src/gp_sitemap/__init__.py: * Import ``logging`` from stdlib and add ``logging.getLogger(__name__).addHandler(logging.NullHandler())`` alongside the existing ``logger = getLogger(__name__)`` from ``sphinx.util.logging``. The Sphinx adapter is kept because it supports the ``type=``/``subtype=`` kwargs used elsewhere for warning classification; the NullHandler attaches to the underlying stdlib logger with the same name so the library is well-behaved when imported outside Sphinx. Inline comment memorializes the two-getLogger dance so future readers don't try to delete one. * ``_write_sitemap`` "skipping sitemap" info message: trailing period removed. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0: 1199 passed, 3 skipped - just build-docs: build succeeded
…ules why: PR #22 code review flagged three ``from <stdlib> import <name>`` import statements that violate CLAUDE.md line 457: Use namespace imports for standard library modules: `import enum` instead of `from enum import Enum` Exception: `dataclasses` module may use `from dataclasses import dataclass, field` for cleaner decorator syntax Every existing workspace package follows this — e.g. gp-sphinx's own config.py uses ``import pathlib`` / ``import tomllib`` then ``pathlib.Path(...)`` / ``tomllib.loads(...)``. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * ``from types import NoneType`` -> ``import types`` + ``types.NoneType`` at each call site (four occurrences inside ``frozenset({...})`` literals for add_config_value). * ``from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit`` -> ``import urllib.parse`` with each call qualified: ``urllib.parse.urljoin(...)``, ``urllib.parse.urlparse(...)``, ``urllib.parse.urlsplit(...)``, ``urllib.parse.urlunsplit(...)``. - packages/gp-opengraph/src/gp_opengraph/_title.py: * ``from html.parser import HTMLParser`` -> ``import html.parser``; subclass declaration becomes ``class HTMLTextParser(html.parser.HTMLParser)``. - packages/gp-opengraph/src/gp_opengraph/_meta.py: * Same HTMLParser rewrite as _title.py. Left as-is (already compliant): - ``from xml.etree import ElementTree`` in gp_sitemap — this imports a *submodule*, not a name, and is the canonical idiom (equivalent to ``import xml.etree.ElementTree as ElementTree``). CLAUDE.md's example targets ``from enum import Enum`` (name import), not submodule aliasing. - ``from collections.abc import Set`` / ``Iterable`` inside ``TYPE_CHECKING`` blocks — gp_sphinx itself does this at config.py#L17, so the convention allows type-only imports this way. No behavior change; purely an import refactor. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0 -vvv: 1199 passed, 3 skipped - just build-docs: build succeeded
why: planning to drop the transitive sphinxext-opengraph (matplotlib bloat) and sphinx-sitemap (multiprocessing.Queue machinery, fragile monkey-patch) deps in favor of two in-workspace packages aligned with gp-sphinx's monorepo conventions. This scaffolding commit establishes the package skeletons so subsequent commits can port the parsers, extension hooks, and functional tests without touching workspace plumbing. what: - Add packages/gp-opengraph/ and packages/gp-sitemap/ with pyproject.toml, README.md, src/<module>/__init__.py, and py.typed markers. Each exposes a minimal setup(app) -> dict returning version plus parallel_read_safe=True and parallel_write_safe=True — importable but no event hooks connected yet. - Register both new packages in the root workspace: add them to [tool.uv.sources] (workspace pins), [dependency-groups] dev, [tool.ruff.lint.isort] known-first-party, and [tool.pytest.ini_options] testpaths. - Wire into CI registries: smoke_gp_opengraph + smoke_gp_sitemap in scripts/ci/package_tools.py, and add both to _PACKAGE_SMOKE_RUNNERS. - Add stub docs pages docs/packages/gp-opengraph.md and docs/packages/gp-sitemap.md so existing test_docs_package_pages_exist_for_every_workspace_package passes. - Add redirect entries for extensions/gp-opengraph and extensions/gp-sitemap in docs/redirects.txt. - Extend the expected-package assertion in tests/test_package_reference.py and the doctest set in docs/_ext/package_reference.py. - Add tests/ext/opengraph/test_importable.py and tests/ext/sitemap/test_importable.py — each asserts the module imports and setup() returns the expected metadata shape. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (165 source files) - uv run py.test --reruns 0: 1166 passed, 3 skipped - just build-docs: build succeeded
why: three small pure-function helpers from upstream sphinxext-opengraph are solid as-is — clean docutils NodeVisitor for prose extraction, stdlib HTMLParser for title-text stripping, stdlib HTMLParser for detecting existing <meta name="description">. Porting them verbatim (with gp-sphinx NumPy docstring style and dispatch_*() signatures widened to nodes.Node for mypy strict) lands the non-controversial scaffolding before the hook-composition commit, keeping diffs reviewable. what: - packages/gp-opengraph/src/gp_opengraph/_description.py: port sphinxext/opengraph/_description_parser.py. Behavior identical; dispatch_visit and dispatch_departure widened from nodes.Element to nodes.Node (upstream typing violated Liskov under mypy strict; gp-sphinx is strict). Adds module docstring, NumPy-style docstrings on every public and protocol method. - packages/gp-opengraph/src/gp_opengraph/_title.py: port sphinxext/opengraph/_title_parser.py. Behavior identical; adds NumPy docstrings. - packages/gp-opengraph/src/gp_opengraph/_meta.py: port sphinxext/opengraph/_meta_parser.py. Behavior identical; return annotation widened from bool to str | bool | None (upstream's bool was wrong — the function returns the content string on hit). - tests/ext/opengraph/test_description.py: build doctrees via docutils RstParser; verify first-paragraph extraction, known-title skip, admonition skip, code-block skip, length truncation with ellipsis, sub-3 cap skips ellipsis. - tests/ext/opengraph/test_title.py: verify round-trip, tag stripping, nested tags, empty input. - tests/ext/opengraph/test_meta.py: verify content extraction, no-content fallback to True, absent description returns None, multi-tag picking. No Sphinx hook is connected yet — parsers are importable and unit-tested but unused. Commit 3 wires them into html-page-context. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (171 source files) - uv run py.test --reruns 0: 1183 passed, 3 skipped - just build-docs: build succeeded
…emission
why: complete the OpenGraph port. Scaffolding (commit 1) established the
package; parsers (commit 2) landed the doctree-walking helpers. This
commit wires everything together: setup() registers every ogp_*
config value the upstream sphinxext-opengraph exposed (minus the
unused ogp_social_cards key's behavior) and connects the
html-page-context handler that emits the tag block. The drop is
drop-in compatible with upstream conf.py files.
Social cards: ogp_social_cards is still accepted as a config value
(so existing conf.py files do not error) but is ignored — gp-opengraph
does not bundle a card generator. A one-line logger.warning fires when
a truthy ogp_social_cards dict is configured, pointing users at the
static-image workflow.
what:
- packages/gp-opengraph/src/gp_opengraph/__init__.py rewrite:
* html_page_context hook that skips epub builds and pages without
doctrees; otherwise appends the computed tag block to
context['metatags'].
* get_tags() composes og:title, og:type, og:url, og:site_name,
og:description (+ <meta name="description"> when not already
present and ogp_enable_meta_description is True), og:image,
og:image:alt, arbitrary og:* overrides from per-page field
lists, and any ogp_custom_meta_tags verbatim.
* Per-page frontmatter (Sphinx field-list / MyST front matter)
honored: ogp_disable, ogp_description_length, og:image,
og:image:alt, og:*.
* READTHEDOCS_CANONICAL_URL fallback via _ambient_site_url() when
ogp_site_url is unset and the RTD env var is present.
* ogp_use_first_image=True scans the doctree for the first image
with an IMAGE_MIME_TYPES-known suffix; relative URLs resolved
against page_url (for first-image) or ogp_site_url (for static
ogp_image).
* _warn_if_social_cards_used logger hook connected via
config-inited.
* setup() registers all eleven ogp_* config values (types
frozenset({...}) uniform), connects html-page-context +
config-inited, returns version + parallel flags both True.
- tests/ext/opengraph/conftest.py: build_og_site fixture backed by
the shared-scenario cache; returns an OgBuildResult NamedTuple
with the completed SharedSphinxResult and a parsed meta-tag dict.
- tests/ext/opengraph/test_meta_emission.py: NamedTuple MetaCase +
pytest.mark.parametrize with ids=test_id. Five cases cover
bare-defaults, with-image-and-alt, custom-meta-tags-emit-verbatim,
site-name-disabled, description-absent-is-not-an-error. Plus a
standalone test that the extracted description contains body
text but not the page title.
- tests/ext/opengraph/test_deprecation_warning.py: NamedTuple
WarnCase + parametrize. Four cases cover empty-dict, None,
enable-True, populated-dict — warning fires iff the value is
truthy.
- tests/ext/opengraph/test_importable.py: scaffold smoke test
upgraded to verify setup() registers every expected config value
and connects both event hooks (uses a small _FakeApp recorder).
CI gate (all green before commit):
- uv run ruff check . --fix --show-fixes: clean
- uv run ruff format .: 1 file reformatted (the rewrite)
- uv run mypy: Success (174 source files)
- uv run py.test --reruns 0: 1193 passed, 3 skipped
- just build-docs: build succeeded
…dioms
why: replace the scaffold setup() placeholder with the full sitemap
generator. Behavior matches sphinx-sitemap v2.9.0 — same config keys
(site_url, sitemap_url_scheme, sitemap_locales, sitemap_filename,
sitemap_excludes, sitemap_show_lastmod, sitemap_indent), same XML
output shape (<urlset> with per-page <url>, optional <lastmod>, and
xhtml:link hreflang alternates). Three modernizations along the way:
1. env.temp_data["gp_sitemap_links"] is a plain list[tuple[str, str|None]]
rather than a multiprocessing.Queue. Sphinx joins parallel workers
before build-finished fires, so the Queue machinery was over-
engineered for a synchronization problem that does not exist.
2. Builder-kind detection uses the public app.builder.name == "dirhtml"
rather than monkey-patching env.is_directory_builder.
3. html_baseurl registration uses contextlib.suppress(ExtensionError)
rather than a bare except BaseException.
Also:
- All add_config_value calls use types=frozenset({...}) uniform.
- Removed upstream dead line that called site_url.rstrip("/") + "/"
without assigning the result (quirk kept the value intact anyway,
but drops dead code).
- _write_sitemap returns early on build failure (checking exception
parameter) rather than trying to write a partial sitemap.
what:
- packages/gp-sitemap/src/gp_sitemap/__init__.py rewrite:
* setup() registers all seven sitemap_* config values plus an
optional html_baseurl (suppressed if already registered by Sphinx
core). Loads sphinx_last_updated_by_git when sitemap_show_lastmod
is True, disabling the feature with a warning on failure.
* _init_link_store (builder-inited) creates env.temp_data list.
* _collect_page_link (html-page-context) collects (link, lastmod)
per page, honoring the sitemap_excludes fnmatch list. Dirhtml
collapses "index" -> "" and "foo/index" -> "foo/".
* _write_sitemap (build-finished) composes the <urlset>, iterating
the list, formatting URLs via sitemap_url_scheme, and emitting
xhtml:link rel=alternate per locale.
* _resolve_locales honors explicit sitemap_locales (with [None]
meaning primary language only); falls back to scanning
locale_dirs sub-directories via pathlib.
* _hreflang_formatter replaces "_" with "-" for hreflang compat.
- tests/ext/sitemap/conftest.py: build_sitemap_site fixture backed
by the shared-scenario cache; builds a 3-page project
(index/about/draft) and returns a SitemapBuildResult NamedTuple
with a parsed ElementTree when the sitemap was written.
- tests/ext/sitemap/test_urlset.py: NamedTuple SitemapCase +
parametrize with ids=test_id. Four cases cover html-suffixes
(html builder), slash-suffixes (dirhtml builder — including
the index-as-empty-segment and language-prefix behavior when
Sphinx's default language="en" is active), sitemap_excludes
drops draft pages, and sitemap_indent=2 pretty-prints without
breaking loc values. Plus two standalone tests: root-urlset
namespace check, and no-site_url emits a warning and skips the
file.
- tests/ext/sitemap/test_importable.py: upgraded smoke test to
verify setup() registers each sitemap_* config value and connects
all three event hooks via a _FakeApp recorder.
CI gate (all green before commit):
- uv run ruff check . --fix --show-fixes: clean
- uv run ruff format .: no changes
- uv run mypy: Success (176 source files)
- uv run py.test --reruns 0: 1199 passed, 3 skipped
- just build-docs: build succeeded
why: swap the transitive sphinxext-opengraph dep for in-workspace
gp-opengraph (matplotlib-free) and add gp-sitemap as a new default so
every gp-sphinx-based docs site gets a sitemap.xml out of the box. Both
packages are drop-in compatible at the conf.py level (same ogp_* /
sitemap_* config keys), so downstream consumers do not need to change
their conf.py.
what:
- packages/gp-sphinx/src/gp_sphinx/defaults.py:
* DEFAULT_EXTENSIONS swaps "sphinxext.opengraph" -> "gp_opengraph"
and appends "gp_sitemap". Length bumps from 12 to 13; doctest
updated.
- packages/gp-sphinx/src/gp_sphinx/config.py:
* merge_sphinx_config auto-wires a new conf["site_url"] (normalized
to a trailing slash) from docs_url so gp-sitemap's URL templating
produces valid URLs without extra conf.py work.
* Docstring updated to mention the new auto-wiring.
- packages/gp-sphinx/pyproject.toml:
* Remove sphinxext-opengraph, add gp-opengraph==0.0.1a9 and
gp-sitemap==0.0.1a9 workspace pins.
- docs/configuration.md: update the DEFAULT_EXTENSIONS reference row
so the docs match reality.
- packages/gp-sitemap/src/gp_sitemap/__init__.py:
* Move the optional sphinx_last_updated_by_git loader from setup()
into a config-inited hook (_maybe_enable_git_lastmod). Accessing
app.config.sitemap_show_lastmod inside setup() raised
AttributeError during the gp-sphinx docs build because app.config
is not a populated Config object at that stage; deferring to
config-inited is safer and matches the Sphinx 8.1+ event model.
- uv.lock regenerated to reflect the new workspace deps.
Verified side effects on the gp-sphinx docs build:
- Sitemap.xml written with 30 <url> entries across all pages
(packages, project, whats-new, generated indices) — dirhtml builder
with "en/0.0.1a9/" lang+version segments from the default
sitemap_url_scheme.
- OG meta present on index.html: og:title, og:type, og:url,
og:site_name, og:description all emitted correctly.
CI gate (all green before commit):
- uv run ruff check . --fix --show-fixes: clean
- uv run ruff format .: no changes
- uv run mypy: Success (176 source files)
- uv run py.test --reruns 0: 1199 passed, 3 skipped
- just build-docs: build succeeded; sitemap.xml written; OG meta
present
why: flesh out the per-package READMEs and docs-site pages from the
commit-1 scaffolding stubs, and add a CHANGES entry so the workspace
v0.0.1a9 release notes capture the swap.
what:
- packages/gp-opengraph/README.md:
* Remove the "Scaffolding" placeholder.
* Add a "What it emits" section listing every tag.
* Add a full config-key table (key, type, default, purpose) covering
all ten ogp_* values the extension registers.
* Add a "Per-page overrides" section showing MyST frontmatter usage.
* Add a "Differences from sphinxext-opengraph" section calling out
ogp_social_cards being accepted-but-ignored.
* Add a "Static images per page" section documenting the migration
path from auto-generated cards to explicit PNGs.
- packages/gp-sitemap/README.md:
* Remove the scaffold placeholder.
* Add a "What it emits" section covering html vs dirhtml output
differences, locale alternates, and optional git lastmod.
* Add a full config-key table.
* Add a "URL templating" section explaining sitemap_url_scheme.
* Add a "Multi-language sites" section with sitemap_locales usage.
* Add a "Differences from sphinx-sitemap" section listing the three
modernizations (no Queue, no monkey-patch, narrow except).
- docs/packages/gp-opengraph.md + docs/packages/gp-sitemap.md:
* Rewrite from the scaffold stubs into proper package pages
modeled on the existing sphinx-ux-badges page — package-meta
directive + Alpha admonition + narrative + package-reference
directive at the bottom.
- CHANGES:
* New Features bullet for gp-opengraph (with the "drop-in for
sphinxext-opengraph minus matplotlib" framing) and for gp-sitemap
(with the three Sphinx 8.1+ modernizations and the
DEFAULT_EXTENSIONS addition).
* New Features bullet under gp-sphinx noting the new auto-wiring
of site_url for gp-sitemap.
CI gate (all green before commit):
- uv run ruff check . --fix --show-fixes: clean
- uv run ruff format .: no changes
- uv run mypy: Success (176 source files)
- uv run py.test --reruns 0: 1199 passed, 3 skipped
- just build-docs: build succeeded; rendered pages include the two
new package entries and the CHANGES entry
why: CI's docs job and the gp-sphinx smoke test both run
sphinx-build -W (warnings-as-errors). Two issues surfaced against the
seo-packages branch that the local just build-docs (no -W) let through:
1. docs/packages/gp-opengraph.md and docs/packages/gp-sitemap.md were
not in any toctree on the main docs site. Sphinx emits
[toc.not_included] as a warning, which -W promotes.
2. gp-sitemap logged a WARNING when site_url (and html_baseurl) were
unset. That matched upstream sphinx-sitemap behavior, but because
gp-sitemap is in gp-sphinx's DEFAULT_EXTENSIONS it runs on every
consumer build — including the smoke test, which builds a minimal
project through merge_sphinx_config() without docs_url. Under -W
that warning broke the smoke build.
what:
- docs/index.md: add a new "SEO" toctree caption referencing
packages/gp-opengraph and packages/gp-sitemap so the two new pages
are toctree-included (no change to the rendered nav — hidden=true,
matches the existing Internal/Utils/UX caption pattern).
- packages/gp-sitemap/src/gp_sitemap/__init__.py:
* Demote the "site_url is required" message from logger.warning to
logger.info with a one-line explanatory comment on why
(DEFAULT_EXTENSIONS semantics differ from an opt-in extension).
* Message reworded from "required — sitemap not built" to "skipping
sitemap — set site_url or html_baseurl to enable" so the log line
reads as informational rather than punitive.
- tests/ext/sitemap/test_urlset.py: rename
test_no_site_url_emits_warning_and_no_sitemap ->
test_no_site_url_skips_sitemap_silently and flip the assertion to
"site_url not in built.result.warnings" with a docstring explaining
the -W compatibility reason.
CI gate (all green before commit, plus local -W repro):
- uv run ruff check . --fix --show-fixes: clean
- uv run ruff format .: no changes
- uv run mypy: Success (176 source files)
- uv run py.test --reruns 0: 1199 passed, 3 skipped
- just build-docs: build succeeded
- uv run sphinx-build -W -b html docs _build/html-strict: succeeds
(was failing on the two toctree warnings before this commit)
…iling periods why: PR #22 code review flagged two CLAUDE.md#logging violations in the two new packages. These are house-style invariants that every existing workspace package (gp_sphinx, sphinx_fonts, sphinx_ux_badges, …) already follows: 1. "Add ``NullHandler`` in library ``__init__.py`` files" (CLAUDE.md line 553) — both new package __init__.py files declared a module-level logger without one. Importing either package outside a Sphinx build could surface a "no handlers could be found" warning from stdlib logging. 2. "No trailing punctuation" in log messages (CLAUDE.md line 576) — two log calls introduced in commit ab3a6a6 and 04f0121 ended with a period. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * After ``logger = logging.getLogger(__name__)`` attach ``logger.addHandler(logging.NullHandler())`` to match the sphinx-fonts pattern exactly. * ``_warn_if_social_cards_used``: message reworded to "ogp_social_cards ignored — gp-opengraph ships no card generator; use a static PNG via ogp_image (site default) or per-page 'og:image' frontmatter" (no trailing period; also trimmed the "is ignored — gp-opengraph does not bundle a card generator" phrasing into "ignored — … ships no card generator" which reads cleaner as a warning). - packages/gp-sitemap/src/gp_sitemap/__init__.py: * Import ``logging`` from stdlib and add ``logging.getLogger(__name__).addHandler(logging.NullHandler())`` alongside the existing ``logger = getLogger(__name__)`` from ``sphinx.util.logging``. The Sphinx adapter is kept because it supports the ``type=``/``subtype=`` kwargs used elsewhere for warning classification; the NullHandler attaches to the underlying stdlib logger with the same name so the library is well-behaved when imported outside Sphinx. Inline comment memorializes the two-getLogger dance so future readers don't try to delete one. * ``_write_sitemap`` "skipping sitemap" info message: trailing period removed. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0: 1199 passed, 3 skipped - just build-docs: build succeeded
…ules why: PR #22 code review flagged three ``from <stdlib> import <name>`` import statements that violate CLAUDE.md line 457: Use namespace imports for standard library modules: `import enum` instead of `from enum import Enum` Exception: `dataclasses` module may use `from dataclasses import dataclass, field` for cleaner decorator syntax Every existing workspace package follows this — e.g. gp-sphinx's own config.py uses ``import pathlib`` / ``import tomllib`` then ``pathlib.Path(...)`` / ``tomllib.loads(...)``. what: - packages/gp-opengraph/src/gp_opengraph/__init__.py: * ``from types import NoneType`` -> ``import types`` + ``types.NoneType`` at each call site (four occurrences inside ``frozenset({...})`` literals for add_config_value). * ``from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit`` -> ``import urllib.parse`` with each call qualified: ``urllib.parse.urljoin(...)``, ``urllib.parse.urlparse(...)``, ``urllib.parse.urlsplit(...)``, ``urllib.parse.urlunsplit(...)``. - packages/gp-opengraph/src/gp_opengraph/_title.py: * ``from html.parser import HTMLParser`` -> ``import html.parser``; subclass declaration becomes ``class HTMLTextParser(html.parser.HTMLParser)``. - packages/gp-opengraph/src/gp_opengraph/_meta.py: * Same HTMLParser rewrite as _title.py. Left as-is (already compliant): - ``from xml.etree import ElementTree`` in gp_sitemap — this imports a *submodule*, not a name, and is the canonical idiom (equivalent to ``import xml.etree.ElementTree as ElementTree``). CLAUDE.md's example targets ``from enum import Enum`` (name import), not submodule aliasing. - ``from collections.abc import Set`` / ``Iterable`` inside ``TYPE_CHECKING`` blocks — gp_sphinx itself does this at config.py#L17, so the convention allows type-only imports this way. No behavior change; purely an import refactor. CI gate (all green before commit): - uv run ruff check . --fix --show-fixes: clean - uv run ruff format .: no changes - uv run mypy: Success (176 source files) - uv run py.test --reruns 0 -vvv: 1199 passed, 3 skipped - just build-docs: build succeeded
why: defaults.py now ships gp_opengraph and gp_sitemap in
DEFAULT_EXTENSIONS, so the workspace is fourteen packages organised
into four tiers, not twelve / three. The {workspace-package-grid}
directive auto-rendered the new packages but the prose paragraph
above it drifted out of sync.
what:
- Update count from "Twelve workspace packages in three tiers" to
"Fourteen workspace packages in four tiers"
- Add a fourth tier bullet group "SEO" listing gp-opengraph and
gp-sitemap, with a note that they auto-load when docs_url is set
why: env_version lets Sphinx invalidate cached doctrees if the extension ever begins persisting env-scoped data; declaring it now is a one-line forward-compat hedge with zero runtime cost. The ExtensionMetadata return annotation aligns gp-opengraph with the precedent already set in sphinx-autodoc-docutils and sphinx-autodoc-sphinx, so mypy resolves the extension contract through the typed Sphinx API instead of a generic dict. what: - Add ExtensionMetadata to the TYPE_CHECKING import block - Change setup() return annotation from dict[str, t.Any] to ExtensionMetadata; sync the NumPy "Returns" docstring section - Add "env_version": 1 to the dict returned by setup()
…config reference why: same shape as the sphinx-autodoc-fastmcp commit — the prior description= pass made the api_* descriptions live, but the docs page still rendered a hand-written table with one-word "Meaning" cells (e.g. "Folds large field-list sections"). Replace it with the autoconfigvalue directive so the new prose descriptions surface and the table can never drift from the live registrations. what: - docs/packages/sphinx-ux-autodoc-layout.md: under the existing "## Configuration" heading, drop the hand-written 4-row table and replace with an eval-rst block invoking .. autoconfigvalue-index:: sphinx_ux_autodoc_layout followed by .. autoconfigvalues:: sphinx_ux_autodoc_layout - All 4 api_* keys (api_layout_enabled, api_fold_parameters, api_collapsed_threshold, api_signature_show_annotations) now render with their full descriptions, types, defaults, and rebuild scopes from the live registrations
Code reviewFound 3 issues:
gp-sphinx/tests/ext/sitemap/test_urlset.py Lines 83 to 96 in 61d12c2 gp-sphinx/tests/ext/opengraph/test_meta_emission.py Lines 92 to 96 in 61d12c2
gp-sphinx/tests/ext/sitemap/conftest.py Lines 52 to 57 in 61d12c2 gp-sphinx/tests/ext/opengraph/conftest.py Lines 71 to 76 in 61d12c2
gp-sphinx/packages/sphinx-gp-sitemap/src/sphinx_gp_sitemap/__init__.py Lines 382 to 386 in 61d12c2 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
why: tests in tests/ext/sitemap/test_urlset.py and tests/ext/opengraph/test_meta_emission.py invoke fixtures (build_sitemap_site, build_og_site) that call build_shared_sphinx_result(), which constructs a real Sphinx app. CLAUDE.md is explicit: "Any test that constructs a Sphinx app … counts" as integration, and "Always mark with @pytest.mark.integration". Without the marker, these tests are indistinguishable from unit tests in selection / reporting and the hot-path "lightest level wins" rule loses its enforcement target. what: - tests/ext/sitemap/test_urlset.py: add module-level `pytestmark = pytest.mark.integration` so every test in the file inherits the marker - tests/ext/opengraph/test_meta_emission.py: same module-level marker
why: both fixtures defaulted to function scope (inherited from the
tmp_path fixture they injected), so each parametrized case rebuilt
the Sphinx app from scratch. CLAUDE.md is unambiguous: "Always use a
module-scoped (or session-scoped) fixture for the build — never
function-scoped" and "No function-scoped Sphinx build fixtures —
always module- or session-scoped." The content-hash cache inside
build_shared_sphinx_result() short-circuits identical scenarios
within the same module once the fixture's cache_root is shared, so
moving to module scope keeps the cache hits and stops thrashing the
fixture for every test case.
what:
- tests/ext/sitemap/conftest.py: add scope="module" to
build_sitemap_site; replace tmp_path: pathlib.Path with
tmp_path_factory: pytest.TempPathFactory; compute
cache_root = tmp_path_factory.mktemp("sitemap-build") once outside
the closure so every _build() call within the module shares it;
drop the now-unused derive_sphinx_scenario_cache_root import
- tests/ext/opengraph/conftest.py: same shape with
cache_root = tmp_path_factory.mktemp("opengraph-build"); drop the
derive_sphinx_scenario_cache_root import (and the now-unused
pathlib import that ruff auto-fixed)
- Test count unchanged (1203 passed); fixture is now reused across
all parametrized cases per module rather than rebuilt each time
…ppress sentinel why: _resolve_locales used `if configured == [None]: return []` to honour the documented `sitemap_locales = [None]` opt-out. Sphinx's types= argument is advisory — when a user wrote `sitemap_locales = (None,)` (tuple) the sentinel check failed, the function returned [None] from list(configured), and downstream _hreflang_formatter(None) crashed with TypeError on `"_" in lang`. Tuple vs list is invisible to most users; the sentinel should accept either spelling. Also strip stray Nones from non-sentinel values rather than passing them through to a guaranteed crash. what: - packages/sphinx-gp-sitemap/src/sphinx_gp_sitemap/__init__.py: _resolve_locales now treats any sequence whose elements are all None as the suppress-hreflang sentinel (`if all(item is None for item in configured): return []`), matching both the documented `[None]` and the easily-mistyped `(None,)`. For non-sentinel sequences, filter out any individual None elements so a partially-malformed config produces a clean hreflang list rather than a crash - Updated the docstring to describe the broadened sentinel contract; the existing README documentation still calls out the list spelling as canonical
…sages why: CLAUDE.md's logging standard says "Lowercase, past tense for events: 'config merged', 'extension resolved'. No trailing punctuation. Keep messages short; put details in extra, not the message string." Two messages in the SEO packages embedded user instructions in the message body — "set site_url or html_baseurl in conf.py to enable" and "use a static PNG via ogp_image (site default) or per-page 'og:image' frontmatter" — which read as imperative directives rather than past-tense event records. Trim both to the event itself; the user-facing remediation already lives in the package docs and README. what: - packages/sphinx-gp-sitemap/src/sphinx_gp_sitemap/__init__.py: rewrite the missing-URL info from "skipping sitemap — set site_url or html_baseurl in conf.py to enable" to "sitemap skipped — site_url and html_baseurl both unset" - packages/sphinx-gp-opengraph/src/sphinx_gp_opengraph/__init__.py: rewrite the social-cards warning from "ogp_social_cards ignored — sphinx-gp-opengraph ships no card generator; use a static PNG via ogp_image (site default) or per-page 'og:image' frontmatter" to "ogp_social_cards ignored — sphinx-gp-opengraph ships no card generator". The "ogp_social_cards" substring is preserved so the existing test_deprecation_warning grep still matches - packages/sphinx-gp-opengraph/README.md: update the verbatim warning quote to match the trimmed text; the surrounding prose now points readers at the next section for the static-image workflow rather than embedding the instruction in the warning
…e URL
why: _write_sitemap built each <loc> as
``site_url + scheme.format(...)``. The scheme defaults to a flat
``{link}`` under gp-sphinx and ``sitemap_link`` itself starts
without a leading slash, so when ``site_url`` lacked a trailing
slash the concatenation produced URLs like
``https://example.comindex.html``. gp-sphinx normalizes its
auto-derived ``site_url`` to end in ``/``, but users who set
``html_baseurl`` directly (and let it fall through as the fallback)
bypass that path and shipped malformed sitemaps.
what:
- packages/sphinx-gp-sitemap/src/sphinx_gp_sitemap/__init__.py:
in _write_sitemap, after the resolution chain
(``site_url or html_baseurl``), append ``/`` when the resolved
value lacks one. The check is unconditional so it also catches
any future override path, not just the html_baseurl fallback.
Existing gp-sphinx site_url paths already end in ``/``, so the
no-op cost is one .endswith call per build
…oc directives
why: Pages that invoke autoconfigvalue / autodirective-index alongside
{package-reference} render the surface twice — once in the homegrown
table without descriptions, once in the autodoc block with them.
sphinx-autodoc-sphinx and sphinx-autodoc-docutils are the canonical
owners; the homegrown directive should keep only the install-adjacent
conf snippet and metadata block.
what:
- package_reference_markdown(): drop config_values / directives / roles /
lexers / themes table generation; keep conf snippet, package metadata,
and the gp-sphinx coordinator surface pointer (now under "Public
surface" so the heading is unambiguous).
- Delete the now-orphan theme_options() helper and its configparser
import; the sphinx-gp-theme page already hand-documents theme.conf.
- Refresh the module docstring to credit autoconfigvalue / autodirective
/ autorole as the surface owners.
- Replace the table-content assertions in tests/test_package_reference.py
with conf-snippet checks plus a regression test asserting that surface
table headings no longer appear.
…ia sphinx-autodoc-docutils why: The page documents 7 directives and 8 roles by example only — no canonical reference of what's registered. With sphinx-autodoc-docutils already in the docs build, autodirective-index and autorole-index can generate the canonical reference straight from app.add_directive() / app.add_role() calls instead of asking readers to scan setup() in the package source. what: - Add a "Directive and role reference" section before "Package reference" that invokes autodirective-index and autorole-index over sphinx_autodoc_fastmcp, mirroring the autoconfigvalue-index + autoconfigvalues pattern used for config values.
…d role discovery why: AutoDirectiveIndex / AutoDirectives (and the role equivalents) introspected the module passed to them for Directive subclasses / *_role callables. That breaks for any extension whose classes live in a submodule (sphinx_autodoc_fastmcp._directives, sphinx_autodoc_sphinx._directives) — passing the package name to ``autodirective-index :: <pkg>`` returns nothing. It also derives the displayed directive name from the class name, which produces 'autoconfigvalueindex' for AutoconfigvalueIndexDirective and 'fastmcptool' for FastMCPToolDirective. Reading the names a package actually registers via its setup() is the source of truth. what: - Add _SetupRecorder / _replay_setup that captures every app.add_* call when a module's setup() runs against a stand-in. __getattr__ swallows everything (add_node, connect, setup_extension, etc.) so unrelated registrations don't crash the recorder. - Add _registered_directives / _registered_roles helpers that prefer the recorder result and fall back to the existing introspection helpers when a module has no setup(). - AutoDirectiveIndex, AutoDirectives, AutoRoleIndex, AutoRoles all route through the new helpers. Each now accepts either an extension package or a directive- / role-defining module. The rendered Python path follows directive_cls.__module__ / role_fn.__module__ so it matches what 'autodirective :: pkg._directives.SomeDirective' would emit. - Add coverage in tests/ext/autodoc_docutils/test_directives.py for both the recorder path (fastmcp) and the introspection fallback (sphinx_autodoc_docutils._directives, sphinx_autodoc_argparse.roles), plus rich-block emission per pair.
…utodoc-docutils why: The extension itself registers four documentation directives (autoconfigvalue, autoconfigvalues, autoconfigvalue-index, autoconfigvalue-page). The page documented them only by example. With the register-aware autodirective-index landing in sphinx-autodoc-docutils, the canonical reference can render directly from the live setup() calls. what: - Replace the closing "The extension itself registers documentation directives ..." paragraph with a "Directive reference" section that invokes ``.. autodirective-index:: sphinx_autodoc_sphinx`` before the existing "Package reference" anchor.
…ve index why: The autodirective-index / autorole-index calls landed on sphinx-autodoc-fastmcp and sphinx-autodoc-sphinx pages emit only the 3-column summary table — a TOC, not a reference. The descriptor blocks (signature pill, type badge, Python path field-list, required/optional arguments, has-content flag, options sub-block) live behind the autodirectives / autoroles directives that the prior register-aware refactor unlocked. Wire them up so each page carries both the scannable index and the per-item reference. what: - sphinx-autodoc-fastmcp page: append ``.. autodirectives:: sphinx_autodoc_fastmcp`` and ``.. autoroles:: sphinx_autodoc_fastmcp`` to the existing eval-rst block under "Directive and role reference"; refresh the lead-in paragraph. - sphinx-autodoc-sphinx page: append ``.. autodirectives:: sphinx_autodoc_sphinx`` to the "Directive reference" eval-rst block; drop the now-redundant single-class ``.. autodirective:: AutoconfigvalueDirective`` demo from the Live demos section since the descriptor blocks below render the same surface for every directive.
…setup() replay fails why: _replay_setup catches all exceptions from a package's setup() and silently degrades to _directive_classes / _role_callables introspection. That fallback routes through _registered_name's class-name mangler, which produces incorrect names like 'autoconfigvalueindex' for AutoconfigvalueIndexDirective — exactly the bug commit 8b5b361 introduced register-aware discovery to fix. A future setup() regression (broken import, attribute typo) would silently revert the rendered docs to the broken names. CLAUDE.md calls for DEBUG-level logging on internal-mechanics events; emit one so the failure is recoverable from the build log. what: - Add module logger and emit logger.debug(..., exc_info=True) on the except branch of _replay_setup before returning None. The message names the module that failed and reports that introspection fallback is active. - Add a regression test that injects a fake module with a raising setup() and asserts a DEBUG-level "setup replay failed" record with caplog.at_level scoped to the module's logger.
why: PR #22 grew beyond the original opengraph/sitemap scope to include register-aware discovery in sphinx-autodoc-docutils and the silent-fallback fix that surfaces setup() failures. Both are public-facing changes downstream consumers will care about, but neither was reflected in the changelog. what: - Add a Features sub-section under sphinx-autodoc-docutils describing the new accept-a-package-name behaviour for the four index/full directives. - Add a Bug fixes sub-section noting the DEBUG breadcrumb that prevents silent regression to mis-derived class-name kebab-casing.
why: Every autodirective-index / autodirectives / autorole-index / autoroles invocation calls _replay_setup, so a docs build with N package pages × M directive invocations re-imports + re-replays each package's setup() for every call. The recorder is read-only by contract — consumers iterate recorder.calls and never mutate — so caching the recorder per module name is safe. what: - Wrap _replay_setup with functools.cache so subsequent calls for the same module name return the same recorder object. - Document the read-only contract and the cache-clear escape hatch in the docstring. - Add a regression test asserting the cache returns the identical recorder on a second call, and update the DEBUG-breadcrumb test to call cache_clear() before and after so it is robust against re-runs in the same pytest session.
…replay_setup; consolidate docs/_ext recorder why: docs/_ext/package_reference.py defined its own RecorderApp + manual setup-replay pattern that drifted from the SetupRecorder / replay_setup helpers introduced in 8b5b361 for sphinx-autodoc-docutils. Two implementations of the same recorder is a maintenance hazard; worse, package_reference.py paid the per-call import + replay cost that 19ab7b2 just optimised away inside sphinx-autodoc-docutils. what: - Promote _SetupRecorder → SetupRecorder and _replay_setup → replay_setup in sphinx_autodoc_docutils._directives; re-export from the package __init__.py with an explicit __all__ so they are part of the public surface. - docs/_ext/package_reference.py imports SetupRecorder + replay_setup from sphinx_autodoc_docutils. RecorderApp stays as an alias so the existing setup() doctest still works without a rewrite. - collect_extension_surface() and _register_extension_objects() call replay_setup directly. Both now share the cache benefit and the DEBUG breadcrumb on setup() failure. - object_path() doctest switched from RecorderApp (now a re-export) to SurfaceDict so the expected ~package_reference.* path is real. - Tests use the new public names.
…olve why: merge_sphinx_config has accepted source_branch (default "main") since e655064, but make_linkcode_resolve in the same module ignored it and hardcoded /blob/master/ for dev versions. Modern projects (including gp-sphinx itself) ship from main, so the resolver emitted broken [source] links pointing at a non-existent master branch. what: - Add source_branch: str = "main" parameter to make_linkcode_resolve alongside the existing src_dir parameter; document it in the NumPy-style docstring. - Replace the literal "/blob/master/" fragment with the parameter so callers control the dev-version URL the same way they control the release-version URL (which already used the version tag). - Add tests/test_config.py::test_make_linkcode_resolve_uses_source_branch exercising a fake module with a dev __version__ to assert the passed source_branch flows through to the resolved URL.
…ing nesting level why: HTMLTextParser tracks tag nesting via handle_starttag / handle_endtag, but Python's HTMLParser fires only handle_starttag for HTML void elements (br, img, hr, meta, link, input, area, base, col, embed, param, source, track, wbr) — there is no closing tag, so handle_endtag never fires. self.level was incrementing on each void element and never decrementing, so all text after a title's first <br> or <img> ended up classified as inside-a-tag and was dropped from text_outside_tags. The same bug exists in upstream sphinxext-opengraph. what: - Skip the level increment in handle_starttag when the tag matches the canonical HTML5 void-element set, so text after void elements is correctly counted as outside-a-tag. - Add a regression test asserting "text<br>more text<img>final" produces "textmore textfinal" with self.level returning to 0.
…xplicitly
why: setup() returned only {"parallel_read_safe": True} with the
intent of opting out of parallel writes. But Sphinx's Extension
class (sphinx/extension.py:38) defaults the missing kwarg to True,
so getattr(ext, "parallel_write_safe", None) returns True, the
parallel-write gate at sphinx/application.py:1828 passes, and the
html-page-context handler _collect_page_link runs in worker
processes whose env.temp_data is never merged. Reproducible: a
serial build emits the full sitemap (e.g. 32 URLs); the same build
under sphinx-build -j 2 emits a small fraction (e.g. 6).
what:
- Return "parallel_write_safe": False alongside parallel_read_safe
so Sphinx's is_parallel_allowed("write") returns False and the
whole build falls back to serial writes. Sitemap collection then
aggregates correctly in env.temp_data.
- Update the module docstring's modernizations list to explain the
default-True trap and explicitly state the False declaration.
- Update docs/packages/sphinx-gp-sitemap.md's Trade-offs section so
users see the same correction in the public docs.
- tests/ext/sitemap/test_importable.py: replace the
"parallel_write_safe not in meta" assertion with the explicit
"parallel_write_safe is False" check, with a comment naming the
Extension default-True trap so the regression cannot silently
reappear.
- CHANGES: brief Bug fixes sub-section under sphinx-gp-sitemap.
…te_url why: merge_sphinx_config() assigned ogp_site_url = docs_url raw, while site_url already got trailing-slash normalisation. urllib's urljoin drops the last path segment of a base URL that lacks a trailing slash, so docs_url="https://example.org/docs" produced "https://example.org/page.html" (missing /docs) for both per-page canonical URLs and image URLs in sphinx-gp-opengraph. Sites hosted at a path silently emitted broken Open Graph metadata. what: - Hoist the normalisation into a single local variable and assign it to both ogp_site_url and site_url so the two stay in lock-step going forward. Comment names the urljoin trap so it cannot silently re-occur. - Update the merge_sphinx_config doctest expectation for ogp_site_url to include the trailing slash. - Add tests/test_config::test_merge_sphinx_config_ogp_site_url_preserves_path_component exercising docs_url with a path and a live urljoin assertion that the resulting URL keeps the path component intact. - Update the from-docs_url table in docs/configuration.md so ogp_site_url advertises the same trailing-slash normalisation as site_url. - CHANGES: brief Bug fixes sub-section under gp-sphinx.
why: _make_tag() only replaced double quotes ("→"), so titles
and site names containing & < > emitted invalid attribute markup
straight into the page head — e.g. <meta property="og:title"
content="AT&T" />. Descriptions were safe because _description.py
called html.escape(text, quote=True) pre-emptively, but every other
tag passing through _make_tag() (og:title, og:site_name,
og:image:alt, custom field-list values) was unsafe.
what:
- _make_tag() now runs html.escape(content, quote=True) so &, <, >,
", and ' all reach the page head as HTML entities. The function
becomes the single boundary for attribute escaping; every existing
and future caller is safe by construction.
- _description.py drops its pre-emptive html.escape() call (and the
now-unused html import). Descriptions flow through _make_tag() the
same as every other field and get escaped exactly once instead of
twice (& would otherwise become &amp;).
- Add NamedTuple cases in tests/ext/opengraph/test_meta_emission.py
exercising titles with & and site names with < >. The first case
also asserts the description is single-escaped (not &amp;) as
a regression guard for the double-escape that the _description.py
removal addresses.
- CHANGES: brief Bug fixes sub-section under sphinx-gp-opengraph.
why: Format audit found section-name violations (### Features → ### What's new), a non-canonical ### Workspace packages section holding entries that belong under What's new, four bug-fix entries leaking implementation depth past the 2-4 line cap, a Raises triple-fix that should split per shipped surface, and six entries missing their PR refs. what: - Rename ### Features → ### What's new and fold the ### Workspace packages section into it; reorder so every "New package: X" entry leads, then per-package change entries. - Tighten the four bloated bug-fix entries (sphinx-gp-opengraph HTML escape, sphinx-gp-sitemap parallel-write, gp-sphinx docs_url path, sphinx-autodoc-docutils setup-replay breadcrumb) to the cap, leading with the user-visible result and pushing implementation depth into the linked PR. Same treatment for sphinx-autodoc-fastmcp decorator-registration and import-failure entries. - Split sphinx-autodoc-typehints-gp's three-fix Raises bundle into three sub-sections (`:exc:` shortener, generics-comma split, empty Examples/References rubric). - Promote `Initial release: gp-sphinx` heading to canonical `New package: gp-sphinx`; prefix `Fonts:` and `Badges:` entries with their package names. - Add PR refs for the six entries that lacked them, mapping each to its originating PR via gh pr list.
…nv.found_docs + get_target_uri why: The per-page html-page-context handler had two correctness regressions on top of the parallel-write trap d7a1104 patched: 1. Incremental builds: Sphinx fires html-page-context only for re-written pages, so editing a single .rst file produced a sitemap with one URL and dropped every other page. 2. URL construction: rebuilding URLs from pagename + html_file_suffix diverges from what the HTML builder emits in <a href> links. html_link_suffix (used by the builder) can differ from html_file_suffix; pagenames with spaces or other reserved characters are URL-quoted by the builder but not by the manual path. Reproduction: pagename "my page" emits "my page.html" via the old path versus "my%20page.html" from get_target_uri. Sphinx's app.builder.get_target_uri(pagename) is the canonical API for "what URL does the builder emit for this page" and lives at sphinx/builders/html/__init__.py:1067. Iterating app.env.found_docs (the env-merged set of all documented files) at build-finished gives complete coverage on incremental and parallel builds without per-handler aggregation logic. what: - Drop _init_link_store, _collect_page_link, env.temp_data list, and the _LINKS_KEY / nodes import. Only config-inited and build-finished handlers remain. - _write_sitemap iterates sorted(app.env.found_docs) at build-finished and calls app.builder.get_target_uri(pagename) per page, plus a hasattr guard that skips non-HTML-family builders. The dirhtml branch folds into get_target_uri's own per-builder routing. - Switch parallel_write_safe back to True with a comment naming the architectural reason: build-finished always runs in the main process and found_docs is part of the env Sphinx merges across parallel-read workers. - Update module docstring's modernizations list and the docs/packages/sphinx-gp-sitemap.md Event hooks + Trade-offs sections to describe the new architecture (build-finished + get_target_uri, two handlers, parallel_write_safe = True). - Update tests/ext/sitemap/test_importable.py: parallel_write_safe is now True; builder-inited and html-page-context hooks are no longer connected. - Rewrite the CHANGES bug-fix entry to describe the actual fix shipped (architectural rework) instead of the parallel_write_safe declaration that d7a1104 used as a band-aid. Manual verification: serial sphinx-build, parallel sphinx-build -j 4, and incremental sphinx-build (touch single .md file) all emit the same complete 27-URL sitemap on this repo's docs.
…TML self-close why: 6393215 added a void-element guard to handle_starttag so HTML5 forms like <br> and <img> stop incrementing self.level. But Python's html.parser.HTMLParser routes XHTML self-closing forms (<br/>, <img/>) through handle_startendtag, whose default impl calls BOTH handle_starttag AND handle_endtag. Filtering only the start path leaves the unbalanced end decrement: get_title("before<br/>after") landed self.level at -1 and produced text_outside_tags = "before" — silently dropping every chunk after the first XHTML self-closing void tag. what: - Lift the void-element set into a module-level _VOID_ELEMENTS frozenset so handle_starttag and handle_endtag share one source of truth (was a literal set in handle_starttag only). - handle_endtag skips the level decrement when the tag is in the void set, mirroring handle_starttag's guard. - Add test_get_title_with_xhtml_self_closing_void_elements covering <br/>, <img/>, <hr/> — title text after each must reach text_outside_tags.
…tion
why: collect_extension_surface() indexed args[N] directly for every
add_directive / add_directive_to_domain / add_role / add_role_to_domain
/ add_lexer / add_html_theme / add_crossref_type call. Sphinx's app
APIs accept both positional and keyword forms — e.g.
app.add_directive("foo", Foo) AND app.add_directive(name="foo", cls=Foo).
A consumer that only reads positional args raises IndexError on the
keyword form and silently misses the registration on the keyword
form for the not-yet-indexed slots. add_config_value already split
the difference (positional-or-kwargs lookup); the rest didn't.
what:
- Add a local _extract_arg(index, key, args, kwargs) helper mirroring
the helper in sphinx_autodoc_docutils._directives. Returns the
positional arg when present, else the kwarg, else None.
- Refactor every args[N] indexing site in collect_extension_surface
to call _extract_arg with both the positional slot and the canonical
Sphinx keyword name. Each branch skips the registration when a
required argument is missing rather than raising IndexError.
- Add tests/test_package_reference covering the helper's three modes:
positional-first, kwarg fallback, missing-returns-None.
why: Re-audit against the changelog skill's "depth out of changelog" rule found ~15 entries leaking implementation mechanic — the cause of a fix rather than its effect on the user. Worst offenders were the entries written this session (sitemap rework, register-aware discovery, XHTML void tags, DEBUG breadcrumb), which all read like PR descriptions rather than upgrade-time decisions. what: - Lead each entry with the user-visible result (what you GAIN, what you can DO, what you need to KNOW); push the cause / mechanic into the linked PR. Most entries trim from 5–10 lines to 2–4. - Drop signal-name / private-symbol / internal-flow leaks: nesting- level counter, env.temp_data routing, app.builder method names, per-handler aggregation, monkey-patching, no-Queue, dirhtml detection mechanic, BadgeNode <abbr>→<span> swap, copy-button template null state, etc. - Tighten the eight-line New package: sphinx-autodoc-pytest-fixtures entry to one product paragraph (autodoc fixtures, badges, plugin page from one directive). Same treatment for the integrated- design-system, fastmcp, sitemap-new-package, typehints-gp-new -package, and ux-badges entries. - Sitemap bug-fix loses the architectural prose; reads as the user-visible win — incremental and parallel builds emit a complete sitemap; URLs match what's on the page.
Summary
sphinx-gp-opengraph: workspace package — drop-in replacement forsphinxext-opengraphminus the matplotlib-based social-card generator.ogp_social_cardsis accepted-but-ignored with a one-line warning pointing at the static-image workflow.sphinx-gp-sitemap: workspace package — drop-in replacement forsphinx-sitemapwith Sphinx 8.1+ idioms.gp-sphinx:DEFAULT_EXTENSIONSswaps"sphinxext.opengraph"→"sphinx_gp_opengraph"and adds"sphinx_gp_sitemap".merge_sphinx_config(docs_url=...)auto-derivesogp_site_url,ogp_site_name,ogp_image,site_url, andsitemap_url_scheme(flat"{link}") from one parameter.sphinx-autodoc-docutils:autodirective-index/autodirectives/autorole-index/autorolesaccept an extension package name and surface each entry under the name the package actually registers, instead of guessing from class names.Motivation
gp-sphinxused to pullsphinxext-opengraphas a transitive dep, which drags matplotlib (~60 MB) for a per-page social-card generator most git-pull projects don't use. It also left gp-sphinx without a sitemap.xml generator — a standard SEO affordance for documentation sites.Bringing both in-house as workspace packages drops the matplotlib dependency from gp-sphinx's baseline while keeping every
ogp_*config key working, adds sitemap.xml emission as a default so every gp-sphinx site is more discoverable, modernises both extensions to Sphinx 8.1+ idioms, and aligns them with gp-sphinx's house conventions (NumPy docstrings,from __future__ import annotations,src/<module>/layout,py.typed, hatchling builds, workspace-pinned0.0.1a9).Drop-in compatibility
Every
ogp_*key the upstreamsphinxext-opengraphregistered remains, with identical semantics — exceptogp_social_cards, which is accepted but ignored (with a one-line warning). Everysitemap_*key the upstreamsphinx-sitemapregistered remains, with identical XML output shape. Downstream consumers should not need anyconf.pychanges.What shipped
CHANGESis the canonical record. The branch has Breaking changes (Sphinx 8.1 floor; CSSgp-sphinx-*namespace migration; five package renames includingsphinx-argparse-neo→sphinx-autodoc-argparse), What's new (the two new packages plus the integrated autodoc design system entries), and Bug fixes (the multi-round audit findings).Verification
Manual sitemap verification on this repo's docs:
sphinx-build, parallelsphinx-build -j 4, and incrementalsphinx-build(after touching one source file) all emit the same complete 27-URLsitemap.xml. The earlier per-pagehtml-page-contextcollection silently dropped pages on incremental and parallel builds; thebuild-finished+app.env.found_docs+app.builder.get_target_uri()path fixes both.docs/_build/html/index.htmlcarries the expected OG meta —og:title,og:type,og:url,og:site_name,og:descriptionall well-formed, with HTML entities for&/</>/"/'.uv tree | grep -E "sphinxext-opengraph|sphinx-sitemap"returns no output — the upstream packages are gone from the workspace.Pre-merge decisions to sign off
Each of these is intentional, none is a blocker, all are hard-to-undo:
gp-sphinx'spyproject.tomlhard-pinssphinx-gp-opengraph==0.0.1a9andsphinx-gp-sitemap==0.0.1a9, so those two need to land on PyPI before the firstgp-sphinxrelease tag (orpip install gp-sphinxfails resolution). Names cannot be reclaimed once published.sphinx-autodoc-docutils.__all__now exportsSetupRecorderandreplay_setup. Once a release ships these names, downstream consumers can import them — renaming becomes a breaking change.DEFAULT_EXTENSIONSchange is automatic for every consumer. Every gp-sphinx project's next install gets thesphinxext.opengraph→sphinx_gp_opengraphswap and thesphinx_gp_sitemapaddition. Projects that setogp_social_cardsget one WARNING per build; projects withoutsite_urlorhtml_baseurlsee an INFO-level "sitemap skipped" line.git filter-reporename pass). Squash-merge collapses cleanly into one main commit; merge-commit preserves the history. Either works — pick consciously.Non-goals
sphinx-gp-opengraph-cardspackage using Pillow if demand appears.sphinx-gp-*packages, not in scope.gp_og_*namespace.Test plan
sphinx-gp-opengraphparser units (description / title / meta) + functional tests across bare-defaults, image+alt, custom-meta-tags, site-name-disabled, description-absent, HTML-escape, XHTML void self-close, deprecation-warningsphinx-gp-sitemapunits + functional tests across html / dirhtml / excludes / non-zero-indent / no-site-url-skipsphinx-autodoc-docutilsunits for register-aware discovery, kwarg form, debug breadcrumb onsetup()failure-j 4, and incremental sphinx-build invocations