From 7edc05b1ff4afe17492738a672795016e0a9f339 Mon Sep 17 00:00:00 2001
From: Kilian Paul <81327374+Darlokt@users.noreply.github.com>
Date: Fri, 18 Sep 2026 01:58:23 +0200
Subject: [PATCH 1/4] Use canonical RFC URLs independently of Docutils
---
CHANGES.rst | 5 ++++
sphinx/environment/__init__.py | 2 +-
sphinx/roles.py | 10 +++----
.../test_builders/test_build_html_5_output.py | 4 +--
tests/test_markup/test_markup.py | 8 +++---
tests/test_roles.py | 26 ++++++++++++++++++-
6 files changed, 42 insertions(+), 13 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index ad6d698341a..730675e4eaf 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,6 +4,11 @@ Release 9.1.1 (in development)
Bugs fixed
----------
+* #14659: Use the canonical ``https://www.rfc-editor.org/info/`` base URL for
+ the :rst:role:`rfc` role and make generated RFC URLs independent of
+ Docutils' URL template.
+ Patch by Kilian Paul
+
* #14465: LaTeX: PDF build crash since LaTeX June 2026 release if tables are
styled with ``'colorrows'`` (which is the default).
Patch by Jean-François B.
diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py
index c5cc77139f7..4e8b19f52ff 100644
--- a/sphinx/environment/__init__.py
+++ b/sphinx/environment/__init__.py
@@ -63,7 +63,7 @@
'cloak_email_addresses': True,
'pep_base_url': 'https://peps.python.org/',
'pep_references': None,
- 'rfc_base_url': 'https://datatracker.ietf.org/doc/html/',
+ 'rfc_base_url': 'https://www.rfc-editor.org/info/',
'rfc_references': None,
'input_encoding': 'utf-8-sig',
'doctitle_xform': False,
diff --git a/sphinx/roles.py b/sphinx/roles.py
index fdb997314fe..9717a90eaea 100644
--- a/sphinx/roles.py
+++ b/sphinx/roles.py
@@ -360,11 +360,11 @@ def run(self) -> tuple[list[Node], list[system_message]]:
def build_uri(self) -> str:
base_url = self.inliner.document.settings.rfc_base_url
- ret = self.target.partition('#')
- if ret[1]:
- return base_url + self.inliner.rfc_url % int(ret[0]) + '#' + ret[2]
- else:
- return base_url + self.inliner.rfc_url % int(ret[0])
+ number, separator, anchor = self.target.partition('#')
+ uri = f'{base_url}rfc{int(number)}/'
+ if separator:
+ uri += f'#{anchor}'
+ return uri
def _format_rfc_target(target: str, /) -> str:
diff --git a/tests/test_builders/test_build_html_5_output.py b/tests/test_builders/test_build_html_5_output.py
index b712d1415a3..8fb4378bf52 100644
--- a/tests/test_builders/test_build_html_5_output.py
+++ b/tests/test_builders/test_build_html_5_output.py
@@ -147,13 +147,13 @@ def checker(nodes: Iterable[Element]) -> Literal[True]:
),
(
'markup.html',
- ".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']"
+ ".//a[@href='https://www.rfc-editor.org/info/rfc1/']"
"[@class='rfc reference external']/strong",
'RFC 1',
),
(
'markup.html',
- ".//a[@href='https://datatracker.ietf.org/doc/html/rfc1.html']"
+ ".//a[@href='https://www.rfc-editor.org/info/rfc1/']"
"[@class='rfc reference external']/strong",
'Request for Comments #1',
),
diff --git a/tests/test_markup/test_markup.py b/tests/test_markup/test_markup.py
index 4889ed6daa8..30c07d989a0 100644
--- a/tests/test_markup/test_markup.py
+++ b/tests/test_markup/test_markup.py
@@ -212,12 +212,12 @@ def rst_to_latex(rst: str, *, app: SphinxTestApp) -> str:
':rfc:`2324`',
(
'
RFC 2324
'
+ 'href="https://www.rfc-editor.org/info/rfc2324/">RFC 2324'
),
(
'\\sphinxAtStartPar\n'
'\\index{RFC@\\spxentry{RFC}!RFC 2324@\\spxentry{RFC 2324}}'
- '\\sphinxhref{https://datatracker.ietf.org/doc/html/rfc2324.html}'
+ '\\sphinxhref{https://www.rfc-editor.org/info/rfc2324/}'
'{\\sphinxstylestrong{RFC 2324}}'
),
),
@@ -226,13 +226,13 @@ def rst_to_latex(rst: str, *, app: SphinxTestApp) -> str:
':rfc:`2324#section-1`',
(
''
+ 'href="https://www.rfc-editor.org/info/rfc2324/#section-1">'
'RFC 2324 Section 1
'
),
(
'\\sphinxAtStartPar\n'
'\\index{RFC@\\spxentry{RFC}!RFC 2324 Section 1@\\spxentry{RFC 2324 Section 1}}'
- '\\sphinxhref{https://datatracker.ietf.org/doc/html/rfc2324.html\\#section-1}'
+ '\\sphinxhref{https://www.rfc-editor.org/info/rfc2324/\\#section-1}'
'{\\sphinxstylestrong{RFC 2324 Section 1}}'
),
),
diff --git a/tests/test_roles.py b/tests/test_roles.py
index e5ba9b3e5b6..f4c01b91672 100644
--- a/tests/test_roles.py
+++ b/tests/test_roles.py
@@ -2,14 +2,19 @@
from __future__ import annotations
+from types import SimpleNamespace
+from typing import TYPE_CHECKING, cast
from unittest.mock import Mock
import pytest
from docutils import nodes
-from sphinx.roles import EmphasizedLiteral, _format_rfc_target
+from sphinx.roles import RFC, EmphasizedLiteral, _format_rfc_target
from sphinx.testing.util import assert_node
+if TYPE_CHECKING:
+ from docutils.parsers.rst.states import Inliner
+
def test_samp() -> None:
emph_literal_role = EmphasizedLiteral()
@@ -137,3 +142,22 @@ def test_samp() -> None:
)
def test_format_rfc_target(target: str, expected_output: str) -> None:
assert _format_rfc_target(target) == expected_output
+
+
+@pytest.mark.parametrize(
+ ('target', 'expected_uri'),
+ [
+ ('2324', 'https://example.com/rfc/rfc2324/'),
+ ('2324#section-1', 'https://example.com/rfc/rfc2324/#section-1'),
+ ('2324#', 'https://example.com/rfc/rfc2324/#'),
+ ],
+)
+def test_rfc_build_uri(target: str, expected_uri: str) -> None:
+ role = RFC()
+ settings = SimpleNamespace(rfc_base_url='https://example.com/rfc/')
+ role.inliner = cast(
+ 'Inliner', SimpleNamespace(document=SimpleNamespace(settings=settings))
+ )
+ role.target = target
+
+ assert role.build_uri() == expected_uri
From c49551c695a61111547b46fa0e56648742d87d07 Mon Sep 17 00:00:00 2001
From: Kilian Paul <81327374+Darlokt@users.noreply.github.com>
Date: Fri, 18 Sep 2026 02:00:01 +0200
Subject: [PATCH 2/4] 67
Bump ENV_Version to 67 to invalidate cached build environments.
---
sphinx/environment/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py
index 4e8b19f52ff..35d76ecb643 100644
--- a/sphinx/environment/__init__.py
+++ b/sphinx/environment/__init__.py
@@ -76,7 +76,7 @@
# This is increased every time an environment attribute is added
# or changed to properly invalidate pickle files.
-ENV_VERSION = 66
+ENV_VERSION = 67
# config status
CONFIG_UNSET = -1
From feceebdcdbe9c4023efe441d521b36735dabe5ff Mon Sep 17 00:00:00 2001
From: Kilian Paul <81327374+Darlokt@users.noreply.github.com>
Date: Fri, 18 Sep 2026 03:26:44 +0200
Subject: [PATCH 3/4] Add authors
---
AUTHORS.rst | 1 +
1 file changed, 1 insertion(+)
diff --git a/AUTHORS.rst b/AUTHORS.rst
index b702c64c0f9..581d1fd4e68 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -81,6 +81,7 @@ Contributors
* Justus Magin -- napoleon improvements
* Kazuya Take -- ``sphinx.testing.path`` bug fix
* Kevin Dunn -- MathJax extension
+* Kilian Paul -- Docutils compatibility, intl tests, and viewcode fixes
* KINEBUCHI Tomohiko -- typing Sphinx as well as docutils
* Kurt McKee -- documentation updates
* Lars Hupfeldt Nielsen - OpenSSL FIPS mode md5 bug fix
From 132667090f23beabf85a570747679aeec6803b90 Mon Sep 17 00:00:00 2001
From: Kilian Paul <81327374+Darlokt@users.noreply.github.com>
Date: Fri, 18 Sep 2026 03:45:46 +0200
Subject: [PATCH 4/4] Update JavaScript search fixtures for environment version
67
---
tests/js/fixtures/cpp/searchindex.js | 2 +-
tests/js/fixtures/multiterm/searchindex.js | 2 +-
tests/js/fixtures/partial/searchindex.js | 2 +-
tests/js/fixtures/titles/searchindex.js | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/tests/js/fixtures/cpp/searchindex.js b/tests/js/fixtures/cpp/searchindex.js
index 1dac186a7e7..da4c01586b9 100644
--- a/tests/js/fixtures/cpp/searchindex.js
+++ b/tests/js/fixtures/cpp/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{},"docnames":["index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{"sphinx (c++ class)":[[0,"_CPPv46Sphinx",false]]},"objects":{"":[[0,0,1,"_CPPv46Sphinx","Sphinx"]]},"objnames":{"0":["cpp","class","C++ class"]},"objtypes":{"0":"cpp:class"},"terms":{"The":0,"This":0,"becaus":0,"c":0,"can":0,"cardin":0,"challeng":0,"charact":0,"class":0,"descript":0,"drop":0,"engin":0,"fixtur":0,"frequent":0,"generat":0,"index":0,"inflat":0,"mathemat":0,"occur":0,"often":0,"project":0,"punctuat":0,"queri":0,"relat":0,"sampl":0,"search":0,"size":0,"sphinx":0,"term":0,"token":0,"use":0,"web":0},"titles":[""],"titleterms":{}})
\ No newline at end of file
+Search.setIndex({"alltitles":{},"docnames":["index"],"envversion":{"sphinx":67,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{"sphinx (c++ class)":[[0,"_CPPv46Sphinx",false]]},"objects":{"":[[0,0,1,"_CPPv46Sphinx","Sphinx"]]},"objnames":{"0":["cpp","class","C++ class"]},"objtypes":{"0":"cpp:class"},"terms":{"The":0,"This":0,"becaus":0,"c":0,"can":0,"cardin":0,"challeng":0,"charact":0,"class":0,"descript":0,"drop":0,"engin":0,"fixtur":0,"frequent":0,"generat":0,"index":0,"inflat":0,"mathemat":0,"occur":0,"often":0,"project":0,"punctuat":0,"queri":0,"relat":0,"sampl":0,"search":0,"size":0,"sphinx":0,"term":0,"token":0,"use":0,"web":0},"titles":[""],"titleterms":{}})
\ No newline at end of file
diff --git a/tests/js/fixtures/multiterm/searchindex.js b/tests/js/fixtures/multiterm/searchindex.js
index bd732522b3d..5991620b3a9 100644
--- a/tests/js/fixtures/multiterm/searchindex.js
+++ b/tests/js/fixtures/multiterm/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"Main Page":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"At":0,"This":0,"adjac":0,"appear":0,"applic":0,"built":0,"can":0,"check":0,"contain":0,"document":0,"doesn":0,"fixtur":0,"format":0,"function":0,"futur":0,"html":0,"includ":0,"match":0,"messag":0,"multipl":0,"multiterm":0,"order":0,"output":0,"perform":0,"perhap":0,"phrase":0,"project":0,"queri":0,"requir":0,"search":0,"success":0,"support":0,"t":0,"term":0,"test":0,"time":0,"use":0,"will":0,"write":0},"titles":["Main Page"],"titleterms":{"main":0,"page":0}})
\ No newline at end of file
+Search.setIndex({"alltitles":{"Main Page":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":67,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"At":0,"This":0,"adjac":0,"appear":0,"applic":0,"built":0,"can":0,"check":0,"contain":0,"document":0,"doesn":0,"fixtur":0,"format":0,"function":0,"futur":0,"html":0,"includ":0,"match":0,"messag":0,"multipl":0,"multiterm":0,"order":0,"output":0,"perform":0,"perhap":0,"phrase":0,"project":0,"queri":0,"requir":0,"search":0,"success":0,"support":0,"t":0,"term":0,"test":0,"time":0,"use":0,"will":0,"write":0},"titles":["Main Page"],"titleterms":{"main":0,"page":0}})
\ No newline at end of file
diff --git a/tests/js/fixtures/partial/searchindex.js b/tests/js/fixtures/partial/searchindex.js
index 8a65718e2ea..a9d3ec42130 100644
--- a/tests/js/fixtures/partial/searchindex.js
+++ b/tests/js/fixtures/partial/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"sphinx_utils module":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"This":0,"built":0,"confirm":0,"document":0,"function":0,"html":0,"includ":0,"input":0,"javascript":0,"match":0,"partial":0,"possibl":0,"project":0,"provid":0,"restructuredtext":0,"sampl":0,"search":0,"term":0,"titl":0,"use":0},"titles":["sphinx_utils module"],"titleterms":{"modul":0,"sphinx_util":0}})
\ No newline at end of file
+Search.setIndex({"alltitles":{"sphinx_utils module":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":67,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"This":0,"built":0,"confirm":0,"document":0,"function":0,"html":0,"includ":0,"input":0,"javascript":0,"match":0,"partial":0,"possibl":0,"project":0,"provid":0,"restructuredtext":0,"sampl":0,"search":0,"term":0,"titl":0,"use":0},"titles":["sphinx_utils module"],"titleterms":{"modul":0,"sphinx_util":0}})
\ No newline at end of file
diff --git a/tests/js/fixtures/titles/searchindex.js b/tests/js/fixtures/titles/searchindex.js
index fe325c7742d..0d661568511 100644
--- a/tests/js/fixtures/titles/searchindex.js
+++ b/tests/js/fixtures/titles/searchindex.js
@@ -1 +1 @@
-Search.setIndex({"alltitles":{"Main Page":[[0,null]],"Relevance":[[0,"relevance"],[1,null]],"Result Scoring":[[0,"result-scoring"]]},"docnames":["index","relevance"],"envversion":{"sphinx":66,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst","relevance.rst"],"indexentries":{"example (class in relevance)":[[0,"relevance.Example",false]],"module":[[0,"module-relevance",false]],"relevance":[[0,"index-1",false],[0,"module-relevance",false]],"relevance (relevance.example attribute)":[[0,"relevance.Example.relevance",false]],"scoring":[[0,"index-0",true]]},"objects":{"":[[0,0,0,"-","relevance"]],"relevance":[[0,1,1,"","Example"]],"relevance.Example":[[0,2,1,"","relevance"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:attribute"},"terms":{"A":1,"By":0,"For":[0,1],"In":[0,1],"This":0,"align":0,"also":1,"answer":0,"appear":1,"area":0,"ask":0,"assign":0,"attempt":0,"attribut":0,"built":1,"can":[0,1],"class":0,"code":[0,1],"collect":0,"consid":1,"contain":0,"context":0,"corpus":1,"demonstr":0,"describ":1,"detail":1,"determin":[0,1],"docstr":0,"document":[0,1],"domain":1,"dure":0,"engin":0,"evalu":0,"exampl":[0,1],"extract":0,"feedback":0,"find":0,"found":0,"function":1,"handl":0,"happen":1,"head":0,"help":0,"high":[0,1],"improv":0,"inform":0,"intend":0,"issu":[0,1],"knowledg":0,"languag":1,"less":1,"like":[0,1],"mani":0,"match":0,"mention":1,"name":[0,1],"numer":0,"object":0,"often":0,"one":[0,1],"onli":[0,1],"order":0,"page":1,"part":1,"particular":0,"present":0,"printf":1,"program":1,"project":0,"queri":[0,1],"question":0,"re":0,"relat":0,"research":0,"result":1,"retriev":0,"s":[0,1],"say":0,"search":[0,1],"seem":0,"softwar":1,"sphinx":0,"straightforward":1,"subject":0,"subsect":0,"term":[0,1],"test":0,"text":0,"time":0,"titl":0,"two":0,"typic":0,"use":0,"user":[0,1],"whether":1,"will":0,"within":0,"word":0},"titles":["Main Page","Relevance"],"titleterms":{"main":0,"page":0,"relev":[0,1],"result":0,"score":0}})
\ No newline at end of file
+Search.setIndex({"alltitles":{"Main Page":[[0,null]],"Relevance":[[0,"relevance"],[1,null]],"Result Scoring":[[0,"result-scoring"]]},"docnames":["index","relevance"],"envversion":{"sphinx":67,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst","relevance.rst"],"indexentries":{"example (class in relevance)":[[0,"relevance.Example",false]],"module":[[0,"module-relevance",false]],"relevance":[[0,"index-1",false],[0,"module-relevance",false]],"relevance (relevance.example attribute)":[[0,"relevance.Example.relevance",false]],"scoring":[[0,"index-0",true]]},"objects":{"":[[0,0,0,"-","relevance"]],"relevance":[[0,1,1,"","Example"]],"relevance.Example":[[0,2,1,"","relevance"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:attribute"},"terms":{"A":1,"By":0,"For":[0,1],"In":[0,1],"This":0,"align":0,"also":1,"answer":0,"appear":1,"area":0,"ask":0,"assign":0,"attempt":0,"attribut":0,"built":1,"can":[0,1],"class":0,"code":[0,1],"collect":0,"consid":1,"contain":0,"context":0,"corpus":1,"demonstr":0,"describ":1,"detail":1,"determin":[0,1],"docstr":0,"document":[0,1],"domain":1,"dure":0,"engin":0,"evalu":0,"exampl":[0,1],"extract":0,"feedback":0,"find":0,"found":0,"function":1,"handl":0,"happen":1,"head":0,"help":0,"high":[0,1],"improv":0,"inform":0,"intend":0,"issu":[0,1],"knowledg":0,"languag":1,"less":1,"like":[0,1],"mani":0,"match":0,"mention":1,"name":[0,1],"numer":0,"object":0,"often":0,"one":[0,1],"onli":[0,1],"order":0,"page":1,"part":1,"particular":0,"present":0,"printf":1,"program":1,"project":0,"queri":[0,1],"question":0,"re":0,"relat":0,"research":0,"result":1,"retriev":0,"s":[0,1],"say":0,"search":[0,1],"seem":0,"softwar":1,"sphinx":0,"straightforward":1,"subject":0,"subsect":0,"term":[0,1],"test":0,"text":0,"time":0,"titl":0,"two":0,"typic":0,"use":0,"user":[0,1],"whether":1,"will":0,"within":0,"word":0},"titles":["Main Page","Relevance"],"titleterms":{"main":0,"page":0,"relev":[0,1],"result":0,"score":0}})
\ No newline at end of file