From 79d28395c2f26edd7f8c1f6f5288dfbda3f58fc0 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 4 Jan 2026 22:50:53 +0000 Subject: [PATCH 01/15] Tests: add reference URI test coverage for `singlehtml` builder Specifically, check for duplication of the HTML anchor fragment delimiter character (`#`). Co-authored-by: @etrian-dev --- tests/roots/test-refuris/ch1.rst | 14 +++++++ tests/roots/test-refuris/conf.py | 2 + tests/roots/test-refuris/glossary/term1.rst | 4 ++ tests/roots/test-refuris/glossary/term2.rst | 4 ++ tests/roots/test-refuris/index.rst | 11 +++++ .../test_builders/test_build_html_refuris.py | 42 +++++++++++++++++++ 6 files changed, 77 insertions(+) create mode 100644 tests/roots/test-refuris/ch1.rst create mode 100644 tests/roots/test-refuris/conf.py create mode 100644 tests/roots/test-refuris/glossary/term1.rst create mode 100644 tests/roots/test-refuris/glossary/term2.rst create mode 100644 tests/roots/test-refuris/index.rst create mode 100644 tests/test_builders/test_build_html_refuris.py diff --git a/tests/roots/test-refuris/ch1.rst b/tests/roots/test-refuris/ch1.rst new file mode 100644 index 00000000000..2f2b4167ecd --- /dev/null +++ b/tests/roots/test-refuris/ch1.rst @@ -0,0 +1,14 @@ +Chapter One +=========== + +This chapter discusses :term:`API` design and :term:`REST` principles. + +We use :term:`API` throughout our application to communicate between +different services. + +Section 1.1 +----------- + +More details about :term:`REST` architecture here. + +The :term:`API` should follow RESTful conventions. diff --git a/tests/roots/test-refuris/conf.py b/tests/roots/test-refuris/conf.py new file mode 100644 index 00000000000..b490ae6b4da --- /dev/null +++ b/tests/roots/test-refuris/conf.py @@ -0,0 +1,2 @@ +project = 'Glossary Test' +extensions = [] diff --git a/tests/roots/test-refuris/glossary/term1.rst b/tests/roots/test-refuris/glossary/term1.rst new file mode 100644 index 00000000000..8a279e071fe --- /dev/null +++ b/tests/roots/test-refuris/glossary/term1.rst @@ -0,0 +1,4 @@ +.. glossary:: + + API + Lorem Ipsum. diff --git a/tests/roots/test-refuris/glossary/term2.rst b/tests/roots/test-refuris/glossary/term2.rst new file mode 100644 index 00000000000..8a279e071fe --- /dev/null +++ b/tests/roots/test-refuris/glossary/term2.rst @@ -0,0 +1,4 @@ +.. glossary:: + + API + Lorem Ipsum. diff --git a/tests/roots/test-refuris/index.rst b/tests/roots/test-refuris/index.rst new file mode 100644 index 00000000000..ae3fe764ee0 --- /dev/null +++ b/tests/roots/test-refuris/index.rst @@ -0,0 +1,11 @@ +Main Documentation +================== + +Welcome to the documentation. + +.. toctree:: + :maxdepth: 2 + + ch1 + glossary/term1 + glossary/term2 diff --git a/tests/test_builders/test_build_html_refuris.py b/tests/test_builders/test_build_html_refuris.py new file mode 100644 index 00000000000..22e9c9eb8a1 --- /dev/null +++ b/tests/test_builders/test_build_html_refuris.py @@ -0,0 +1,42 @@ +"""Test output of reference URIs when building single-page HTML output.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from tests.test_builders.xpath_util import check_xpath + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from pathlib import Path + from xml.etree.ElementTree import Element, ElementTree + + from sphinx.testing.util import SphinxTestApp + + +def _internal_reference_fragment_check(nodes: Sequence[Element]) -> None: + """Confirm that internal references do not contain duplicate fragment symbols""" + assert nodes, 'Expected at least one node to check' + for node in nodes: + assert node.tag == 'a', 'Attempted to check hyperlink on a non-anchor element' + href = node.attrib.get('href') + # Allow Sphinx index and table hyperlinks to be non-same-document, as exceptions. + if href in {'genindex.html', 'py-modindex.html', 'search.html'}: + continue + assert not href or href.count('#') < 2, 'Hyperlink contains duplicate fragments' + + +@pytest.mark.sphinx('singlehtml', testroot='refuris') +def test_singlehtml_refuris( + app: SphinxTestApp, + cached_etree_parse: Callable[[Path], ElementTree], +) -> None: + app.build() + check_xpath( + cached_etree_parse(app.outdir / 'index.html'), + 'index.html', + ".//a[@class='reference internal']", + _internal_reference_fragment_check, + ) From d5db93897a038c84afaad28e17c051182dbbffc7 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 4 Jan 2026 20:51:55 +0000 Subject: [PATCH 02/15] Revert "singlehtml: deprecate the 'fix_refuris' helper function (#13037)" This reverts commit c93723b80396959e19442f7058ad3412eaf11468. Conflicts: CHANGES.rst doc/extdev/deprecated.rst sphinx/builders/singlehtml.py --- sphinx/builders/singlehtml.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/sphinx/builders/singlehtml.py b/sphinx/builders/singlehtml.py index 1888f6679d1..f895077df59 100644 --- a/sphinx/builders/singlehtml.py +++ b/sphinx/builders/singlehtml.py @@ -9,7 +9,6 @@ from sphinx._cli.util.colour import darkgreen from sphinx.builders.html import StandaloneHTMLBuilder -from sphinx.deprecation import RemovedInSphinx10Warning from sphinx.environment.adapters.toctree import global_toctree_for_doc from sphinx.locale import __ from sphinx.util import logging @@ -52,14 +51,6 @@ def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str: return self.get_target_uri(to, typ) def fix_refuris(self, tree: Node) -> None: - deprecation_msg = ( - "The 'SingleFileHTMLBuilder.fix_refuris' method is no longer used " - 'within the builder and is planned for removal in Sphinx 10. ' - 'Please report malformed URIs generated by the Sphinx singlehtml ' - 'builder as bugreports.' - ) - warnings.warn(deprecation_msg, RemovedInSphinx10Warning, stacklevel=2) - # fix refuris with double anchor for refnode in tree.findall(nodes.reference): if 'refuri' not in refnode: @@ -86,6 +77,8 @@ def _get_local_toctree( toctree = global_toctree_for_doc( self.env, docname, self, tags=self.tags, collapse=collapse, **kwargs ) + if toctree is not None: + self.fix_refuris(toctree) return self.render_partial(toctree)['fragment'] def assemble_doctree(self) -> nodes.document: @@ -95,6 +88,7 @@ def assemble_doctree(self) -> nodes.document: tree = inline_all_toctrees(self, set(), master, tree, darkgreen, [master]) tree['docname'] = master self.env.resolve_references(tree, master, self) + self.fix_refuris(tree) return tree def assemble_toc_secnumbers(self) -> dict[str, dict[str, tuple[int, ...]]]: @@ -145,6 +139,7 @@ def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, A ) # if there is no toctree, toc is None if toctree: + self.fix_refuris(toctree) toc = self.render_partial(toctree)['fragment'] display_toc = True else: From c32cf72b53e5967d2cfc49017a9dfcebe7dbdeea Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 4 Jan 2026 23:00:20 +0000 Subject: [PATCH 03/15] singlehtml: retain deprecation notice --- sphinx/builders/singlehtml.py | 9 +++++++++ tests/test_builders/test_build_html_refuris.py | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/sphinx/builders/singlehtml.py b/sphinx/builders/singlehtml.py index f895077df59..23712b647a5 100644 --- a/sphinx/builders/singlehtml.py +++ b/sphinx/builders/singlehtml.py @@ -9,6 +9,7 @@ from sphinx._cli.util.colour import darkgreen from sphinx.builders.html import StandaloneHTMLBuilder +from sphinx.deprecation import RemovedInSphinx10Warning from sphinx.environment.adapters.toctree import global_toctree_for_doc from sphinx.locale import __ from sphinx.util import logging @@ -51,6 +52,14 @@ def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str: return self.get_target_uri(to, typ) def fix_refuris(self, tree: Node) -> None: + deprecation_msg = ( + "The 'SingleFileHTMLBuilder.fix_refuris' method is deprecated " + 'within the builder and is planned for removal in Sphinx 10. ' + 'Please report malformed URIs generated by the Sphinx singlehtml ' + 'builder as bugreports.' + ) + warnings.warn(deprecation_msg, RemovedInSphinx10Warning, stacklevel=2) + # fix refuris with double anchor for refnode in tree.findall(nodes.reference): if 'refuri' not in refnode: diff --git a/tests/test_builders/test_build_html_refuris.py b/tests/test_builders/test_build_html_refuris.py index 22e9c9eb8a1..068eab17a53 100644 --- a/tests/test_builders/test_build_html_refuris.py +++ b/tests/test_builders/test_build_html_refuris.py @@ -8,6 +8,8 @@ from tests.test_builders.xpath_util import check_xpath +from sphinx.deprecation import RemovedInSphinx10Warning + if TYPE_CHECKING: from collections.abc import Callable, Sequence from pathlib import Path @@ -33,7 +35,8 @@ def test_singlehtml_refuris( app: SphinxTestApp, cached_etree_parse: Callable[[Path], ElementTree], ) -> None: - app.build() + with pytest.warns(RemovedInSphinx10Warning, match=r'deprecated.'): + app.build() check_xpath( cached_etree_parse(app.outdir / 'index.html'), 'index.html', From a9cda59e015f0dafcab185ca64abd77e0431427a Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 4 Jan 2026 23:08:49 +0000 Subject: [PATCH 04/15] Tests: fixup: run `ruff check --fix` to resolve import ordering --- tests/test_builders/test_build_html_refuris.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_builders/test_build_html_refuris.py b/tests/test_builders/test_build_html_refuris.py index 068eab17a53..93a9dba6d34 100644 --- a/tests/test_builders/test_build_html_refuris.py +++ b/tests/test_builders/test_build_html_refuris.py @@ -6,10 +6,10 @@ import pytest -from tests.test_builders.xpath_util import check_xpath - from sphinx.deprecation import RemovedInSphinx10Warning +from tests.test_builders.xpath_util import check_xpath + if TYPE_CHECKING: from collections.abc import Callable, Sequence from pathlib import Path From 1d50c8be2f5c6c3bb139b07017e1764999cbb2b9 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 13 Aug 2026 09:06:26 +0100 Subject: [PATCH 05/15] Add CHANGES.rst entry --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ad6d698341a..00c18167e73 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,11 @@ Bugs fixed English stemmer) and Dutch (which uses the Dutch Porter stemmer). Patch by Hugo van Kemenade +* #14221: Fix a regression from Sphinx v8.2.0 onwards that could cause + duplicate hyperlink fragment identifiers to be emitted during ``singlehtml`` + project builds. + Patch by James Addison + Release 9.1.0 (released Dec 31, 2025) ===================================== From d93fa097da7a378446754ab56e03998e9e892335 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 13 Aug 2026 20:25:54 +0100 Subject: [PATCH 06/15] Revert "singlehtml: deprecate the 'fix_refuris' helper function (#13037)" This reverts commit c93723b80396959e19442f7058ad3412eaf11468. Conflicts: CHANGES.rst doc/extdev/deprecated.rst sphinx/builders/singlehtml.py --- doc/extdev/deprecated.rst | 5 ----- sphinx/builders/singlehtml.py | 10 ---------- 2 files changed, 15 deletions(-) diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index b7583b0e37d..79323233e6d 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -52,11 +52,6 @@ The following is a list of deprecated interfaces. - 11.0 - N/A - * - ``sphinx.builders.singlehtml.SingleFileHTMLBuilder.fix_refuris`` - - 8.2 - - 10.0 - - N/A - * - ``sphinx.util.FilenameUniqDict`` - 8.1 - 10.0 diff --git a/sphinx/builders/singlehtml.py b/sphinx/builders/singlehtml.py index 23712b647a5..cd55261537d 100644 --- a/sphinx/builders/singlehtml.py +++ b/sphinx/builders/singlehtml.py @@ -2,14 +2,12 @@ from __future__ import annotations -import warnings from typing import TYPE_CHECKING from docutils import nodes from sphinx._cli.util.colour import darkgreen from sphinx.builders.html import StandaloneHTMLBuilder -from sphinx.deprecation import RemovedInSphinx10Warning from sphinx.environment.adapters.toctree import global_toctree_for_doc from sphinx.locale import __ from sphinx.util import logging @@ -52,14 +50,6 @@ def get_relative_uri(self, from_: str, to: str, typ: str | None = None) -> str: return self.get_target_uri(to, typ) def fix_refuris(self, tree: Node) -> None: - deprecation_msg = ( - "The 'SingleFileHTMLBuilder.fix_refuris' method is deprecated " - 'within the builder and is planned for removal in Sphinx 10. ' - 'Please report malformed URIs generated by the Sphinx singlehtml ' - 'builder as bugreports.' - ) - warnings.warn(deprecation_msg, RemovedInSphinx10Warning, stacklevel=2) - # fix refuris with double anchor for refnode in tree.findall(nodes.reference): if 'refuri' not in refnode: From 07228e7a7cb83e48a0fd9dd2b11c69c5988fd198 Mon Sep 17 00:00:00 2001 From: James Addison Date: Thu, 13 Aug 2026 20:32:10 +0100 Subject: [PATCH 07/15] Tests: singlehtml: remove deprecation warning expectation Relates-to commit d93fa097da7a378446754ab56e03998e9e892335. --- tests/test_builders/test_build_html_refuris.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_builders/test_build_html_refuris.py b/tests/test_builders/test_build_html_refuris.py index 93a9dba6d34..22e9c9eb8a1 100644 --- a/tests/test_builders/test_build_html_refuris.py +++ b/tests/test_builders/test_build_html_refuris.py @@ -6,8 +6,6 @@ import pytest -from sphinx.deprecation import RemovedInSphinx10Warning - from tests.test_builders.xpath_util import check_xpath if TYPE_CHECKING: @@ -35,8 +33,7 @@ def test_singlehtml_refuris( app: SphinxTestApp, cached_etree_parse: Callable[[Path], ElementTree], ) -> None: - with pytest.warns(RemovedInSphinx10Warning, match=r'deprecated.'): - app.build() + app.build() check_xpath( cached_etree_parse(app.outdir / 'index.html'), 'index.html', From 3085b2f482fa7199640bb0dd4c3029d0f3b94953 Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 17 Aug 2026 00:15:45 +0100 Subject: [PATCH 08/15] Tests: reduce/simplify the duplicate-definition refuri testroot --- tests/roots/test-refuris/ch1.rst | 14 -------------- tests/roots/test-refuris/index.rst | 6 ++++-- 2 files changed, 4 insertions(+), 16 deletions(-) delete mode 100644 tests/roots/test-refuris/ch1.rst diff --git a/tests/roots/test-refuris/ch1.rst b/tests/roots/test-refuris/ch1.rst deleted file mode 100644 index 2f2b4167ecd..00000000000 --- a/tests/roots/test-refuris/ch1.rst +++ /dev/null @@ -1,14 +0,0 @@ -Chapter One -=========== - -This chapter discusses :term:`API` design and :term:`REST` principles. - -We use :term:`API` throughout our application to communicate between -different services. - -Section 1.1 ------------ - -More details about :term:`REST` architecture here. - -The :term:`API` should follow RESTful conventions. diff --git a/tests/roots/test-refuris/index.rst b/tests/roots/test-refuris/index.rst index ae3fe764ee0..0b8f94f7301 100644 --- a/tests/roots/test-refuris/index.rst +++ b/tests/roots/test-refuris/index.rst @@ -1,11 +1,13 @@ Main Documentation ================== -Welcome to the documentation. +This documentation discusses :term:`API` design. + +We use :term:`API` throughout our application to communicate between +different services. .. toctree:: :maxdepth: 2 - ch1 glossary/term1 glossary/term2 From 800c55ac730e7b7e6c71f0ae7fb7b8be9dac574a Mon Sep 17 00:00:00 2001 From: Jared Dillard Date: Mon, 17 Aug 2026 00:26:58 +0100 Subject: [PATCH 09/15] Tests: singlehtml: add HREF/anchor value test Co-authored-by: James Addison --- .../test_builders/test_build_html_refuris.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/test_builders/test_build_html_refuris.py b/tests/test_builders/test_build_html_refuris.py index 22e9c9eb8a1..d06a049a922 100644 --- a/tests/test_builders/test_build_html_refuris.py +++ b/tests/test_builders/test_build_html_refuris.py @@ -29,7 +29,7 @@ def _internal_reference_fragment_check(nodes: Sequence[Element]) -> None: @pytest.mark.sphinx('singlehtml', testroot='refuris') -def test_singlehtml_refuris( +def test_singlehtml_refuris_check_fragments( app: SphinxTestApp, cached_etree_parse: Callable[[Path], ElementTree], ) -> None: @@ -40,3 +40,25 @@ def test_singlehtml_refuris( ".//a[@class='reference internal']", _internal_reference_fragment_check, ) + + +@pytest.mark.sphinx('singlehtml', testroot='refuris') +def test_singlehtml_refuris_check_term_anchor( + app: SphinxTestApp, + cached_etree_parse: Callable[[Path], ElementTree], +) -> None: + """Note: Sphinx currently lacks support for multiple glossaries; all term + definitions exist in a single namespace, and duplicate names may clobber + each other. + For more details, see: https://github.com/sphinx-doc/sphinx/issues/1399 + """ + app.build() + etree = cached_etree_parse(app.outdir / 'index.html') + api_refs = [ + node + for node in etree.findall(".//a[@class='reference internal']") + if ''.join(node.itertext()) == 'API' + ] + assert api_refs # the API term is referenced twice from the document text + assert all(ref.get('href') == '#term-API' for ref in api_refs) + assert etree.find(".//*[@id='term-API']") is not None From 4f37a34b5fcd3801be974aea8de8c190c834c797 Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 17 Aug 2026 11:08:20 +0100 Subject: [PATCH 10/15] Tests: simplify refuris testroot Duplicate (re)definitions of the same term are not required to replicate #14221 --- tests/roots/test-refuris/glossary/term2.rst | 4 ---- tests/roots/test-refuris/index.rst | 1 - 2 files changed, 5 deletions(-) delete mode 100644 tests/roots/test-refuris/glossary/term2.rst diff --git a/tests/roots/test-refuris/glossary/term2.rst b/tests/roots/test-refuris/glossary/term2.rst deleted file mode 100644 index 8a279e071fe..00000000000 --- a/tests/roots/test-refuris/glossary/term2.rst +++ /dev/null @@ -1,4 +0,0 @@ -.. glossary:: - - API - Lorem Ipsum. diff --git a/tests/roots/test-refuris/index.rst b/tests/roots/test-refuris/index.rst index 0b8f94f7301..c044ca8a538 100644 --- a/tests/roots/test-refuris/index.rst +++ b/tests/roots/test-refuris/index.rst @@ -10,4 +10,3 @@ different services. :maxdepth: 2 glossary/term1 - glossary/term2 From cf5af53f82f7c12817508f5facf5825537e5feee Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 17 Aug 2026 11:08:47 +0100 Subject: [PATCH 11/15] Tests: cleanup: remove misleading comment Duplicate (re)definitions of the same term are not required to replicate #14221 --- tests/test_builders/test_build_html_refuris.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_builders/test_build_html_refuris.py b/tests/test_builders/test_build_html_refuris.py index d06a049a922..048aa04136d 100644 --- a/tests/test_builders/test_build_html_refuris.py +++ b/tests/test_builders/test_build_html_refuris.py @@ -47,11 +47,6 @@ def test_singlehtml_refuris_check_term_anchor( app: SphinxTestApp, cached_etree_parse: Callable[[Path], ElementTree], ) -> None: - """Note: Sphinx currently lacks support for multiple glossaries; all term - definitions exist in a single namespace, and duplicate names may clobber - each other. - For more details, see: https://github.com/sphinx-doc/sphinx/issues/1399 - """ app.build() etree = cached_etree_parse(app.outdir / 'index.html') api_refs = [ From 3207ade5671b9fb1968e0e5195cdb2aee9d97e27 Mon Sep 17 00:00:00 2001 From: Jared Dillard Date: Fri, 21 Aug 2026 01:39:16 -0700 Subject: [PATCH 12/15] Apply suggestion from @jdillard --- tests/roots/test-refuris/glossary/term1.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/roots/test-refuris/glossary/term1.rst b/tests/roots/test-refuris/glossary/term1.rst index 8a279e071fe..4c7cbfdd0dc 100644 --- a/tests/roots/test-refuris/glossary/term1.rst +++ b/tests/roots/test-refuris/glossary/term1.rst @@ -1,3 +1,6 @@ +Glossary +======== + .. glossary:: API From 26155d24401bcd82e3cebb5bc0c036294eb3edd9 Mon Sep 17 00:00:00 2001 From: Jared Dillard Date: Fri, 21 Aug 2026 14:01:44 +0100 Subject: [PATCH 13/15] Tests: cleanup: remove unnecessary check and misleading comment --- tests/test_builders/test_build_html_refuris.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/test_builders/test_build_html_refuris.py b/tests/test_builders/test_build_html_refuris.py index 048aa04136d..951f255585d 100644 --- a/tests/test_builders/test_build_html_refuris.py +++ b/tests/test_builders/test_build_html_refuris.py @@ -22,9 +22,6 @@ def _internal_reference_fragment_check(nodes: Sequence[Element]) -> None: for node in nodes: assert node.tag == 'a', 'Attempted to check hyperlink on a non-anchor element' href = node.attrib.get('href') - # Allow Sphinx index and table hyperlinks to be non-same-document, as exceptions. - if href in {'genindex.html', 'py-modindex.html', 'search.html'}: - continue assert not href or href.count('#') < 2, 'Hyperlink contains duplicate fragments' From 0b069bb21678ddeb23a8758c0e35e46ff8fa391f Mon Sep 17 00:00:00 2001 From: James Addison Date: Fri, 21 Aug 2026 14:04:54 +0100 Subject: [PATCH 14/15] Tests: refactor / simplify assertion --- tests/test_builders/test_build_html_refuris.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_builders/test_build_html_refuris.py b/tests/test_builders/test_build_html_refuris.py index 951f255585d..99ebaf26a68 100644 --- a/tests/test_builders/test_build_html_refuris.py +++ b/tests/test_builders/test_build_html_refuris.py @@ -22,7 +22,9 @@ def _internal_reference_fragment_check(nodes: Sequence[Element]) -> None: for node in nodes: assert node.tag == 'a', 'Attempted to check hyperlink on a non-anchor element' href = node.attrib.get('href') - assert not href or href.count('#') < 2, 'Hyperlink contains duplicate fragments' + if not href: + continue + assert href.count('#') < 2, 'Hyperlink contains duplicate fragments' @pytest.mark.sphinx('singlehtml', testroot='refuris') From 66588280e2ac3966df450b163824af120e40f557 Mon Sep 17 00:00:00 2001 From: James Addison <55152140+jayaddison@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:12:09 +0000 Subject: [PATCH 15/15] Update CHANGES.rst entry: mention withdrawn deprecation of `fix_refuris` Co-authored-by: Jared Dillard --- CHANGES.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 00c18167e73..1e986521c80 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,12 +13,12 @@ Bugs fixed English stemmer) and Dutch (which uses the Dutch Porter stemmer). Patch by Hugo van Kemenade -* #14221: Fix a regression from Sphinx v8.2.0 onwards that could cause - duplicate hyperlink fragment identifiers to be emitted during ``singlehtml`` - project builds. +* #14221: Fix a regression from Sphinx v8.2.0 onwards that could cause links + produced by ``singlehtml`` builds to contain multiple fragment delimiters + (``#``), and withdraw the deprecation of + ``SingleFileHTMLBuilder.fix_refuris`` because the builder still requires it. Patch by James Addison - Release 9.1.0 (released Dec 31, 2025) =====================================