Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion docs/src/about.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ is injected after the ordinary Sphinx build is finished.

Caveats
-------
- **Only works with HTML documentation**, disabled otherwise. If the extension
- **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.
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
3 changes: 2 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 @@ -289,6 +289,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
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
53 changes: 33 additions & 20 deletions tests/extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
any_whitespace = re.compile(r"\s*")
ref_tests = [(p.name, p) for p in Path(__file__).with_name("ref").glob("*.txt")]
ref_xfails = {}
builders = ["html", "dirhtml"]


def assert_links(file: Path, links: list):
Expand All @@ -46,7 +47,8 @@ def assert_links(file: Path, links: list):


@pytest.mark.parametrize(("name", "file"), ref_tests)
def test_references(name: str, file: Path, tmp_path: Path):
@pytest.mark.parametrize("builder", builders)
def test_references(name: str, file: Path, builder: str, tmp_path: Path):
"""
Basic extension tests for reference building.

Expand All @@ -71,7 +73,7 @@ def test_references(name: str, file: Path, tmp_path: Path):

files = {"conf.py": default_conf + conf, "index.rst": index}
print(f"Building file {name}.")
result_dir = _sphinx_build(tmp_path, "html", files)
result_dir = _sphinx_build(tmp_path, builder, files)

assert_links(result_dir / "index.html", links)
assert check_link_targets(result_dir) == len(links)
Expand All @@ -81,7 +83,8 @@ def test_references(name: str, file: Path, tmp_path: Path):


@pytest.mark.parametrize("file", table_tests)
def test_tables(file: Path, tmp_path: Path):
@pytest.mark.parametrize("builder", builders)
def test_tables(file: Path, builder: str, tmp_path: Path):
"""
Tests for backreference tables.

Expand All @@ -107,7 +110,7 @@ def test_tables(file: Path, tmp_path: Path):
links = []

files = {"conf.py": default_conf + conf, "index.rst": index}
result_dir = _sphinx_build(tmp_path, "html", files)
result_dir = _sphinx_build(tmp_path, builder, files)

index_html = result_dir / "index.html"
text = index_html.read_text("utf-8")
Expand All @@ -124,7 +127,8 @@ def test_tables(file: Path, tmp_path: Path):


@pytest.mark.parametrize("file", fail_tests)
def test_fails(file: Path, tmp_path: Path):
@pytest.mark.parametrize("builder", builders)
def test_fails(file: Path, builder: str, tmp_path: Path):
"""
Tests for failing builds.

Expand All @@ -138,7 +142,7 @@ def test_fails(file: Path, tmp_path: Path):
conf, index = file.read_text("utf-8").split("# split")
files = {"conf.py": default_conf + conf, "index.rst": index}
with pytest.raises(RuntimeError):
_sphinx_build(tmp_path, "html", files)
_sphinx_build(tmp_path, builder, files)


def test_non_html_build(tmp_path: Path):
Expand All @@ -158,7 +162,8 @@ def test_non_html_build(tmp_path: Path):
_sphinx_build(tmp_path, "man", files)


def test_build_twice_and_modify_one_file(tmp_path: Path):
@pytest.mark.parametrize("builder", builders)
def test_build_twice_and_modify_one_file(builder: str, tmp_path: Path):
index = """
Test project
------------
Expand Down Expand Up @@ -187,11 +192,12 @@ def test_build_twice_and_modify_one_file(tmp_path: Path):
.. autolink-examples:: test_package.bar
"""
files = {"conf.py": default_conf, "index.rst": index, "another.rst": another}
_sphinx_build(tmp_path, "html", files)
_sphinx_build(tmp_path, "html", {"another.rst": another2})
_sphinx_build(tmp_path, builder, files)
_sphinx_build(tmp_path, builder, {"another.rst": another2})


def test_build_twice_and_delete_one_file(tmp_path: Path):
@pytest.mark.parametrize("builder", builders)
def test_build_twice_and_delete_one_file(builder: str, tmp_path: Path):
conf = default_conf + "\nsuppress_warnings = ['toc.not_readable']"
index = """
Test project
Expand All @@ -215,12 +221,13 @@ def test_build_twice_and_delete_one_file(tmp_path: Path):
"""

files = {"conf.py": conf, "index.rst": index, "another.rst": another}
_sphinx_build(tmp_path, "html", files)
_sphinx_build(tmp_path, builder, files)
(tmp_path / "src" / "another.rst").unlink()
_sphinx_build(tmp_path, "html", {})
_sphinx_build(tmp_path, builder, {})


def test_raise_unexpected(tmp_path: Path):
@pytest.mark.parametrize("builder", builders)
def test_raise_unexpected(builder: str, tmp_path: Path):
index = """
Test project
------------
Expand All @@ -243,13 +250,14 @@ def raise_nomsg(*_, **__):

target = "sphinx_codeautolink.parse.ImportTrackerVisitor"
with pytest.raises(RuntimeError), patch(target, raise_msg):
_sphinx_build(tmp_path, "html", files)
_sphinx_build(tmp_path, builder, files)

with pytest.raises(RuntimeError), patch(target, raise_nomsg):
_sphinx_build(tmp_path, "html", files)
_sphinx_build(tmp_path, builder, files)


def test_parallel_build(tmp_path: Path):
@pytest.mark.parametrize("builder", builders)
def test_parallel_build(builder: str, tmp_path: Path):
index = """
Test project
------------
Expand All @@ -274,14 +282,19 @@ def test_parallel_build(tmp_path: Path):
index = index + "\n ".join(["", *list(subfiles)])
files = {"conf.py": default_conf, "index.rst": index}
files.update({k + ".rst": v for k, v in subfiles.items()})
result_dir = _sphinx_build(tmp_path, "html", files, n_processes=4)
result_dir = _sphinx_build(tmp_path, builder, files, n_processes=4)

for file in subfiles:
assert_links(result_dir / (file + ".html"), links)
if builder == "dirhtml" and file != "index":
assert_links(result_dir / file / "index.html", links)
else:
assert_links(result_dir / (file + ".html"), links)

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


def test_skip_identical_code(tmp_path: Path):
@pytest.mark.parametrize("builder", builders)
def test_skip_identical_code(builder: str, tmp_path: Path):
"""Code skipped and then linked in an identical block after."""
index = """
Test project
Expand All @@ -301,7 +314,7 @@ def test_skip_identical_code(tmp_path: Path):
.. automodule:: test_project
"""
files = {"conf.py": default_conf, "index.rst": index}
result_dir = _sphinx_build(tmp_path, "html", files)
result_dir = _sphinx_build(tmp_path, builder, files)

index_html = result_dir / "index.html"
text = index_html.read_text("utf-8")
Expand Down
4 changes: 4 additions & 0 deletions tests/extension/_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def check_link_targets(root: Path) -> int:
external_site_ids[base] = gather_ids(sub_soup)
ids = external_site_ids[base]
else:
if base == "":
base = str(doc)
elif base == "../":
base = str(doc.parent.parent / "index.html")
ids = site_ids[Path(base)]
assert id_ in ids, (
f"ID {id_} not found in {base}"
Expand Down
Loading