Skip to content

Add DIRHTML builder support #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 6, 2025
Merged
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
4 changes: 2 additions & 2 deletions docs/src/about.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ is injected after the ordinary Sphinx build is finished.

Caveats
-------
- **Only works with HTML documentation**, disabled otherwise. If the extension
is off, it silently removes directives that would produce output.
- **Only works with HTML or DIRHTML documentation**, disabled otherwise. If the
extension is off, it silently removes directives that would produce output.
- **Only processes blocks, not inline code**. Sphinx has great tools
for linking definitions inline, and longer code should be in a block anyway.
- **Doesn't run example code**. Therefore all possible resolvable types are not
Expand Down
2 changes: 1 addition & 1 deletion docs/src/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Caveats
-------
For a more thorough explanation, see :ref:`about`.

- Only works with HTML documentation
- Only works with HTML or DIRHTML documentation
- Only processes blocks, not inline code
- Doesn't run example code
- Parsing and type hint resolving is incomplete
Expand Down
4 changes: 4 additions & 0 deletions docs/src/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ These release notes are based on
sphinx-codeautolink adheres to
`Semantic Versioning <https://semver.org>`_.

Unreleased
----------
- Add support for the DIRHTML Sphinx builder (:issue:`188`)

0.17.2 (2025-03-02)
-------------------
- Support :rst:dir:`testsetup` from ``sphinx.ext.doctest`` as another
Expand Down
4 changes: 3 additions & 1 deletion src/sphinx_codeautolink/extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def __init__(self) -> None:
@print_exceptions()
def build_inited(self, app) -> None:
"""Handle initial setup."""
if app.builder.name != "html":
if app.builder.name not in ("html", "dirhtml"):
self.do_nothing = True
return

Expand Down Expand Up @@ -268,6 +268,7 @@ def generate_backref_tables(self, app, doctree, docname):
visitor = CodeRefsVisitor(
doctree,
code_refs=self.code_refs,
builder=app.builder.name,
warn_no_backreference=self.warn_no_backreference,
)
doctree.walk(visitor)
Expand All @@ -289,6 +290,7 @@ def apply_links(self, app, exception) -> None:
self.inventory,
self.custom_blocks,
self.search_css_classes,
builder_name=app.builder.name,
)

self.cache.write()
Expand Down
8 changes: 7 additions & 1 deletion src/sphinx_codeautolink/extension/backref.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Backreference tables implementation."""

from dataclasses import dataclass
from pathlib import Path

from docutils import nodes

Expand Down Expand Up @@ -62,11 +63,13 @@ def __init__(
self,
*args,
code_refs: dict[str, list[CodeExample]],
builder: str,
warn_no_backreference: bool = False,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)
self.code_refs = code_refs
self.builder = builder
self.warn_no_backreference = warn_no_backreference

def unknown_departure(self, node) -> None:
Expand All @@ -79,7 +82,10 @@ def unknown_visit(self, node) -> None:

items = []
for ref in self.code_refs.get(node.ref, []):
link = ref.document + ".html"
if self.builder == "dirhtml" and Path(ref.document).name != "index":
link = ref.document + "/index.html"
else:
link = ref.document + ".html"
if ref.ref_id is not None:
link += f"#{ref.ref_id}"
items.append((link, " / ".join(ref.headings)))
Expand Down
7 changes: 6 additions & 1 deletion src/sphinx_codeautolink/extension/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,14 @@ def link_html(
inventory: dict,
custom_blocks: dict,
search_css_classes: list,
builder_name: str = "html",
) -> None:
"""Inject links to code blocks on disk."""
html_file = Path(out_dir) / (document + ".html")
if builder_name == "dirhtml" and Path(document).name != "index":
html_file = Path(out_dir) / document / "index.html"
else:
html_file = Path(out_dir) / (document + ".html")

text = html_file.read_text("utf-8")
soup = BeautifulSoup(text, "html.parser")

Expand Down
59 changes: 58 additions & 1 deletion tests/extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ def test_parallel_build(tmp_path: Path):

for file in subfiles:
assert_links(result_dir / (file + ".html"), links)

assert check_link_targets(result_dir) == n_subfiles * len(links)


Expand Down Expand Up @@ -312,14 +313,70 @@ def test_skip_identical_code(tmp_path: Path):
assert "sphinx-codeautolink-a" in str(blocks[1])


def test_dirhtml_builder(tmp_path: Path):
index = """
Test project
============

.. toctree::
:maxdepth: 2

page1/index
page2
subdir/page3

Index Page Code
---------------

.. code:: python

import test_project
test_project.bar()

.. automodule:: test_project
"""

page = """
Page {idx}
===========

.. code:: python

import test_project
test_project.bar()

.. autolink-examples:: test_project.bar
"""

files = {
"conf.py": default_conf,
"index.rst": index,
"page1/index.rst": page.format(idx=1),
"page2.rst": page.format(idx=2),
"subdir/page3.rst": page.format(idx=3),
}
links = ["test_project", "test_project.bar"]

result_dir = _sphinx_build(tmp_path, "dirhtml", files)

assert_links(result_dir / "index.html", links)
assert_links(result_dir / "page1/index.html", links)
assert_links(result_dir / "page2/index.html", links)
assert_links(result_dir / "subdir/page3/index.html", links)

assert check_link_targets(result_dir) == len(links) * 4


def _sphinx_build(
folder: Path, builder: str, files: dict[str, str], n_processes: int | None = None
) -> Path:
"""Build Sphinx documentation and return result folder."""
src_dir = folder / "src"
src_dir.mkdir(exist_ok=True)
for name, content in files.items():
(src_dir / name).write_text(content, "utf-8")
path = src_dir / name
path.parent.mkdir(exist_ok=True, parents=True)
path.write_text(content, "utf-8")

build_dir = folder / "build"
args = ["-M", builder, str(src_dir), str(build_dir), "-W"]
Expand Down
16 changes: 12 additions & 4 deletions tests/extension/_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
def check_link_targets(root: Path) -> int:
"""Validate links in HTML site at root, return number of links found."""
site_docs = {
p.relative_to(root): BeautifulSoup(p.read_text("utf-8"), "html.parser")
p: BeautifulSoup(p.read_text("utf-8"), "html.parser")
for p in root.glob("**/*.html")
}
site_ids = {k: gather_ids(v) for k, v in site_docs.items()}
Expand All @@ -27,10 +27,18 @@ def check_link_targets(root: Path) -> int:
external_site_ids[base] = gather_ids(sub_soup)
ids = external_site_ids[base]
else:
ids = site_ids[Path(base)]
target_path = (doc.parent / base).resolve()
if target_path.is_dir():
target_path /= "index.html"
assert target_path.exists(), (
f"Target path {target_path!s} not found while validating"
f" link for `{link.string}` in {doc.relative_to(root)!s}!"
)
ids = site_ids[target_path]

assert id_ in ids, (
f"ID {id_} not found in {base}"
f" while validating link for `{link.string}` in {doc!s}!"
f"ID {id_} not found in {base} while validating link"
f" for `{link.string}` in {doc.relative_to(root)!s}!"
)
total += 1
return total
Expand Down
Loading