From fee1cf726204f7ead542bf86b6647362cbe7a695 Mon Sep 17 00:00:00 2001 From: rshide <32021884+rshide@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:28:43 -0500 Subject: [PATCH 1/2] Refs #37127 -- Added a version-aware sourcefile Sphinx role. Co-authored-by: Tim Schilling --- docs/_ext/djangodocs.py | 34 ++++++++++ tests/{sphinx => sphinx_tests}/__init__.py | 0 tests/sphinx_tests/test_djangodocs.py | 64 ++++++++++++++++++ .../test_github_links.py | 65 +++++++++---------- .../testdata/package/__init__.py | 0 .../testdata/package/import_error.py | 0 .../testdata/package/module.py | 2 +- .../testdata/package/other_module.py | 0 .../testdata/package/wildcard_base.py | 0 .../testdata/package/wildcard_module.py | 0 tests/sphinx_tests/tests.py | 27 ++++++++ 11 files changed, 157 insertions(+), 35 deletions(-) rename tests/{sphinx => sphinx_tests}/__init__.py (100%) create mode 100644 tests/sphinx_tests/test_djangodocs.py rename tests/{sphinx => sphinx_tests}/test_github_links.py (69%) rename tests/{sphinx => sphinx_tests}/testdata/package/__init__.py (100%) rename tests/{sphinx => sphinx_tests}/testdata/package/import_error.py (100%) rename tests/{sphinx => sphinx_tests}/testdata/package/module.py (83%) rename tests/{sphinx => sphinx_tests}/testdata/package/other_module.py (100%) rename tests/{sphinx => sphinx_tests}/testdata/package/wildcard_base.py (100%) rename tests/{sphinx => sphinx_tests}/testdata/package/wildcard_module.py (100%) create mode 100644 tests/sphinx_tests/tests.py diff --git a/docs/_ext/djangodocs.py b/docs/_ext/djangodocs.py index ac7b161eeece..c42279638b64 100644 --- a/docs/_ext/djangodocs.py +++ b/docs/_ext/djangodocs.py @@ -7,11 +7,13 @@ from docutils import nodes from docutils.parsers.rst import Directive from docutils.statemachine import ViewList +from github_links import get_branch from sphinx import addnodes from sphinx import version_info as sphinx_version from sphinx.directives.code import CodeBlock from sphinx.domains.std import Cmdoption from sphinx.util import logging +from sphinx.util.nodes import split_explicit_title from sphinx.writers.html import HTMLTranslator logger = logging.getLogger(__name__) @@ -62,6 +64,7 @@ def setup(app): app.add_directive("console", ConsoleDirective) app.connect("html-page-context", html_page_context_hook) app.add_role("default-role-error", default_role_error) + app.add_role("sourcefile", sourcefile) return {"parallel_read_safe": True} @@ -372,3 +375,34 @@ def default_role_error( ) logger.warning(msg, location=(inliner.document.current_source, lineno)) return [nodes.Text(text)], [] + + +def sourcefile( + name, + rawtext, + text, + lineno, + inliner, + options=None, + content=None, +): + options = options or {} + + _, title, target = split_explicit_title(text) + + env = inliner.document.settings.env + branch = get_branch( + version=env.config.version, + next_version=env.config.django_next_version, + ) + url = f"https://github.com/django/django/blob/{branch}/{target}" + + literal = nodes.literal(title, title) + reference = nodes.reference( + rawtext, + "", + literal, + refuri=url, + **options, + ) + return [reference], [] diff --git a/tests/sphinx/__init__.py b/tests/sphinx_tests/__init__.py similarity index 100% rename from tests/sphinx/__init__.py rename to tests/sphinx_tests/__init__.py diff --git a/tests/sphinx_tests/test_djangodocs.py b/tests/sphinx_tests/test_djangodocs.py new file mode 100644 index 000000000000..8d95ddcabc5c --- /dev/null +++ b/tests/sphinx_tests/test_djangodocs.py @@ -0,0 +1,64 @@ +import sys +import unittest +from types import SimpleNamespace + +from sphinx_tests.tests import SimpleSphinxTestCase, sphinx + +# The import must happen at the end of setUpClass, so it can't be imported at +# the top of the file. +djangodocs = None + + +@unittest.skipIf(sphinx is None, "sphinx required") +class DjangoDocsTests(SimpleSphinxTestCase): + @classmethod + def docs_module_import(cls): + cls.addClassCleanup(sys.modules.pop, "djangodocs", None) + # Linters/IDEs may not be able to detect this as a valid import. + import djangodocs as _djangodocs + + global djangodocs + djangodocs = _djangodocs + + def sourcefile(self, text, *, version="6.2", next_version="6.2"): + config = SimpleNamespace( + version=version, + django_next_version=next_version, + ) + env = SimpleNamespace(config=config) + settings = SimpleNamespace(env=env) + document = SimpleNamespace(settings=settings) + inliner = SimpleNamespace(document=document) + return djangodocs.sourcefile("sourcefile", "", text, 1, inliner) + + def test_sourcefile_uses_main_branch(self): + role_nodes, messages = self.sourcefile("django/forms/forms.py") + + self.assertEqual(messages, []) + self.assertEqual(role_nodes[0].astext(), "django/forms/forms.py") + self.assertEqual( + role_nodes[0]["refuri"], + "https://github.com/django/django/blob/main/django/forms/forms.py", + ) + + def test_sourcefile_uses_stable_branch(self): + role_nodes, messages = self.sourcefile( + "django/forms/forms.py", version="6.0", next_version="6.2" + ) + + self.assertEqual(messages, []) + self.assertEqual( + role_nodes[0]["refuri"], + "https://github.com/django/django/blob/stable/6.0.x/" + "django/forms/forms.py", + ) + + def test_sourcefile_supports_explicit_title(self): + role_nodes, messages = self.sourcefile("'forms.py' ") + + self.assertEqual(messages, []) + self.assertEqual(role_nodes[0].astext(), "'forms.py'") + self.assertEqual( + role_nodes[0]["refuri"], + "https://github.com/django/django/blob/main/django/forms/forms.py", + ) diff --git a/tests/sphinx/test_github_links.py b/tests/sphinx_tests/test_github_links.py similarity index 69% rename from tests/sphinx/test_github_links.py rename to tests/sphinx_tests/test_github_links.py index db0447b62829..58818673ec0d 100644 --- a/tests/sphinx/test_github_links.py +++ b/tests/sphinx_tests/test_github_links.py @@ -1,7 +1,6 @@ -import pathlib import sys -from django.test import SimpleTestCase +from sphinx_tests.tests import SimpleSphinxTestCase def last_n_parts(path, n): @@ -13,16 +12,9 @@ def last_n_parts(path, n): github_links = None -class GitHubLinkTests(SimpleTestCase): +class GitHubLinkTests(SimpleSphinxTestCase): @classmethod - def setUpClass(cls): - # The file implementing the code under test is in the docs folder and - # is not part of the Django package. This means it cannot be imported - # through standard means. Include its parent in the pythonpath for the - # duration of the tests to allow the code to be imported. - cls.ext_path = str((pathlib.Path(__file__).parents[2] / "docs/_ext").resolve()) - sys.path.insert(0, cls.ext_path) - cls.addClassCleanup(sys.path.remove, cls.ext_path) + def docs_module_import(cls): cls.addClassCleanup(sys.modules.pop, "github_links", None) # Linters/IDEs may not be able to detect this as a valid import. import github_links as _github_links @@ -57,84 +49,86 @@ def test_module_name_to_file_path_module(self): def test_get_path_and_line_class(self): path, line = github_links.get_path_and_line( - module="tests.sphinx.testdata.package.module", fullname="MyClass" + module="tests.sphinx_tests.testdata.package.module", fullname="MyClass" ) self.assertEqual( - last_n_parts(path, 5), "tests/sphinx/testdata/package/module.py" + last_n_parts(path, 5), "tests/sphinx_tests/testdata/package/module.py" ) self.assertEqual(line, 12) def test_get_path_and_line_func(self): path, line = github_links.get_path_and_line( - module="tests.sphinx.testdata.package.module", fullname="my_function" + module="tests.sphinx_tests.testdata.package.module", fullname="my_function" ) self.assertEqual( - last_n_parts(path, 5), "tests/sphinx/testdata/package/module.py" + last_n_parts(path, 5), "tests/sphinx_tests/testdata/package/module.py" ) self.assertEqual(line, 24) def test_get_path_and_line_method(self): path, line = github_links.get_path_and_line( - module="tests.sphinx.testdata.package.module", fullname="MyClass.my_method" + module="tests.sphinx_tests.testdata.package.module", + fullname="MyClass.my_method", ) self.assertEqual( - last_n_parts(path, 5), "tests/sphinx/testdata/package/module.py" + last_n_parts(path, 5), "tests/sphinx_tests/testdata/package/module.py" ) self.assertEqual(line, 16) def test_get_path_and_line_cached_property(self): path, line = github_links.get_path_and_line( - module="tests.sphinx.testdata.package.module", + module="tests.sphinx_tests.testdata.package.module", fullname="MyClass.my_cached_property", ) self.assertEqual( - last_n_parts(path, 5), "tests/sphinx/testdata/package/module.py" + last_n_parts(path, 5), "tests/sphinx_tests/testdata/package/module.py" ) self.assertEqual(line, 20) def test_get_path_and_line_forwarded_import(self): path, line = github_links.get_path_and_line( - module="tests.sphinx.testdata.package.module", fullname="MyOtherClass" + module="tests.sphinx_tests.testdata.package.module", fullname="MyOtherClass" ) self.assertEqual( - last_n_parts(path, 5), "tests/sphinx/testdata/package/other_module.py" + last_n_parts(path, 5), "tests/sphinx_tests/testdata/package/other_module.py" ) self.assertEqual(line, 1) def test_get_path_and_line_wildcard_import(self): path, line = github_links.get_path_and_line( - module="tests.sphinx.testdata.package.module", fullname="WildcardClass" + module="tests.sphinx_tests.testdata.package.module", + fullname="WildcardClass", ) self.assertEqual( last_n_parts(path, 5), - "tests/sphinx/testdata/package/wildcard_module.py", + "tests/sphinx_tests/testdata/package/wildcard_module.py", ) self.assertEqual(line, 4) path, line = github_links.get_path_and_line( - module="tests.sphinx.testdata.package.module", + module="tests.sphinx_tests.testdata.package.module", fullname="WildcardMixin", ) self.assertEqual( last_n_parts(path, 5), - "tests/sphinx/testdata/package/wildcard_base.py", + "tests/sphinx_tests/testdata/package/wildcard_base.py", ) self.assertEqual(line, 1) def test_get_path_and_line_forwarded_import_module(self): path, line = github_links.get_path_and_line( - module="tests.sphinx.testdata.package.module", + module="tests.sphinx_tests.testdata.package.module", fullname="other_module.MyOtherClass", ) self.assertEqual( - last_n_parts(path, 5), "tests/sphinx/testdata/package/other_module.py" + last_n_parts(path, 5), "tests/sphinx_tests/testdata/package/other_module.py" ) self.assertEqual(line, 1) @@ -177,33 +171,36 @@ def test_github_linkcode_resolve_not_found(self): def test_github_linkcode_resolve_link_to_object(self): info = { - "module": "tests.sphinx.testdata.package.module", + "module": "tests.sphinx_tests.testdata.package.module", "fullname": "MyClass", } self.assertEqual( github_links.github_linkcode_resolve( "py", info, version="3.2", next_version="3.2" ), - "https://github.com/django/django/blob/main/tests/sphinx/" + "https://github.com/django/django/blob/main/tests/sphinx_tests/" "testdata/package/module.py#L12", ) def test_github_linkcode_resolve_link_to_class_older_version(self): info = { - "module": "tests.sphinx.testdata.package.module", + "module": "tests.sphinx_tests.testdata.package.module", "fullname": "MyClass", } self.assertEqual( github_links.github_linkcode_resolve( "py", info, version="2.2", next_version="3.2" ), - "https://github.com/django/django/blob/stable/2.2.x/tests/sphinx/" - "testdata/package/module.py#L12", + "https://github.com/django/django/blob/stable/2.2.x/" + "tests/sphinx_tests/testdata/package/module.py#L12", ) def test_import_error(self): - msg = "Could not import '.....test' in 'tests.sphinx.testdata.package'." + msg = ( + "Could not import '.....test' in " "'tests.sphinx_tests.testdata.package'." + ) with self.assertRaisesMessage(ImportError, msg): github_links.get_path_and_line( - module="tests.sphinx.testdata.package.import_error", fullname="Test" + module="tests.sphinx_tests.testdata.package.import_error", + fullname="Test", ) diff --git a/tests/sphinx/testdata/package/__init__.py b/tests/sphinx_tests/testdata/package/__init__.py similarity index 100% rename from tests/sphinx/testdata/package/__init__.py rename to tests/sphinx_tests/testdata/package/__init__.py diff --git a/tests/sphinx/testdata/package/import_error.py b/tests/sphinx_tests/testdata/package/import_error.py similarity index 100% rename from tests/sphinx/testdata/package/import_error.py rename to tests/sphinx_tests/testdata/package/import_error.py diff --git a/tests/sphinx/testdata/package/module.py b/tests/sphinx_tests/testdata/package/module.py similarity index 83% rename from tests/sphinx/testdata/package/module.py rename to tests/sphinx_tests/testdata/package/module.py index 987923b55ee1..0b2498d0e192 100644 --- a/tests/sphinx/testdata/package/module.py +++ b/tests/sphinx_tests/testdata/package/module.py @@ -3,7 +3,7 @@ """ from django.utils.functional import cached_property -from tests.sphinx.testdata.package.wildcard_module import * # noqa +from tests.sphinx_tests.testdata.package.wildcard_module import * # noqa from . import other_module # noqa from .other_module import MyOtherClass # noqa diff --git a/tests/sphinx/testdata/package/other_module.py b/tests/sphinx_tests/testdata/package/other_module.py similarity index 100% rename from tests/sphinx/testdata/package/other_module.py rename to tests/sphinx_tests/testdata/package/other_module.py diff --git a/tests/sphinx/testdata/package/wildcard_base.py b/tests/sphinx_tests/testdata/package/wildcard_base.py similarity index 100% rename from tests/sphinx/testdata/package/wildcard_base.py rename to tests/sphinx_tests/testdata/package/wildcard_base.py diff --git a/tests/sphinx/testdata/package/wildcard_module.py b/tests/sphinx_tests/testdata/package/wildcard_module.py similarity index 100% rename from tests/sphinx/testdata/package/wildcard_module.py rename to tests/sphinx_tests/testdata/package/wildcard_module.py diff --git a/tests/sphinx_tests/tests.py b/tests/sphinx_tests/tests.py new file mode 100644 index 000000000000..865f1df121af --- /dev/null +++ b/tests/sphinx_tests/tests.py @@ -0,0 +1,27 @@ +import pathlib +import sys + +from django.test import SimpleTestCase + +try: + import sphinx +except ImportError: + sphinx = None + + +class SimpleSphinxTestCase(SimpleTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # The file implementing the code under test is in the docs folder and + # is not part of the Django package. This means it cannot be imported + # through standard means. Include its parent in the pythonpath for the + # duration of the tests to allow the code to be imported. + cls.ext_path = str((pathlib.Path(__file__).parents[2] / "docs/_ext").resolve()) + sys.path.insert(0, cls.ext_path) + cls.addClassCleanup(sys.path.remove, cls.ext_path) + cls.docs_module_import() + + @classmethod + def docs_module_import(cls): + """Override this method to allow importing code in the docs folder.""" From 177fb98c1025dd32879380e0665bc238cbfe6819 Mon Sep 17 00:00:00 2001 From: rshide <32021884+rshide@users.noreply.github.com> Date: Tue, 11 Aug 2026 22:28:49 -0500 Subject: [PATCH 2/2] Fixed #37127 -- Added source links for form templates. --- docs/ref/forms/api.txt | 34 ++++++++---- docs/ref/forms/fields.txt | 7 +-- docs/ref/forms/renderers.txt | 18 ++++--- docs/ref/forms/widgets.txt | 99 +++++++++++++++++++++++----------- docs/topics/forms/formsets.txt | 25 +++++---- docs/topics/forms/index.txt | 6 ++- 6 files changed, 126 insertions(+), 63 deletions(-) diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 04816bc5eed6..16f68228da07 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -647,6 +647,9 @@ The template used to render a field's ``