Skip to content
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: 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
----------
- Fix backreference links using relative URIs (:issue:`190`)

0.17.3 (2025-03-06)
-------------------
- Fix Sphinx InventoryItem deprecation warning (:issue:`173`)
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 @@ -271,7 +271,8 @@ def generate_backref_tables(self, app, doctree, docname):
visitor = CodeRefsVisitor(
doctree,
code_refs=self.code_refs,
builder=app.builder.name,
docname=docname,
builder=app.builder,
warn_no_backreference=self.warn_no_backreference,
)
doctree.walk(visitor)
Expand Down
11 changes: 5 additions & 6 deletions src/sphinx_codeautolink/extension/backref.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Backreference tables implementation."""

from dataclasses import dataclass
from pathlib import Path

from docutils import nodes
from sphinx.builders import Builder

from sphinx_codeautolink.warn import logger, warn_type

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

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

items = []
for ref in self.code_refs.get(node.ref, []):
if self.builder == "dirhtml" and Path(ref.document).name != "index":
link = ref.document + "/index.html"
else:
link = ref.document + ".html"
link = self.builder.get_relative_uri(self.docname, ref.document)
if ref.ref_id is not None:
link += f"#{ref.ref_id}"
items.append((link, " / ".join(ref.headings)))
Expand Down
54 changes: 53 additions & 1 deletion tests/extension/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from bs4 import BeautifulSoup
from sphinx.cmd.build import main as sphinx_main

from ._check import check_link_targets
from ._check import check_link_targets, check_reference_targets_exist

# Insert test package root to path for all tests
sys.path.insert(0, str(Path(__file__).parent / "src"))
Expand Down Expand Up @@ -365,6 +365,58 @@ def test_dirhtml_builder(tmp_path: Path):
assert_links(result_dir / "subdir/page3/index.html", links)

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


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

.. toctree::

subdir/page1
subdir/subdir2/page2

Index Page
----------

.. 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,
"subdir/page1.rst": page.format(idx=1),
"subdir/subdir2/page2.rst": page.format(idx=2),
}
links = ["test_project", "test_project.bar"]

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

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

assert check_link_targets(result_dir) == len(links) * 3
check_reference_targets_exist(result_dir)


def _sphinx_build(
Expand Down
19 changes: 19 additions & 0 deletions tests/extension/_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ def check_link_targets(root: Path) -> int:
return total


def check_reference_targets_exist(root: Path):
site_docs = {
p: BeautifulSoup(p.read_text("utf-8"), "html.parser")
for p in root.glob("**/*.html")
}
for doc, soup in site_docs.items():
for link in soup.find_all("a", attrs={"class": "reference internal"}):
base = link["href"].split("#")[0]
if any(base.startswith(s) for s in ("http://", "https://")):
continue
target_path = doc if base == "" else (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}!"
)


def gather_ids(soup: BeautifulSoup) -> set:
"""Gather all HTML IDs from a given page."""
return {tag["id"] for tag in soup.find_all(id=True)}