From 8bcf410dd233c6692c37a1d6da5052d9d9649e1b Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 10:26:31 -0700 Subject: [PATCH 01/32] Improve module path finder --- sphinx/ext/viewcode.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index afc4b06f5cc..e697658c505 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -63,6 +63,25 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: value = getattr(value, attr) return getattr(value, '__module__', None) + except ModuleNotFoundError: + # Attempt to find full path of module + module_path = modname.split('.') + actual_path = __import__(module_path[0], globals(), locals(), [], 0) + if len(module_path) > 1: + for mod in module_path[1:]: + actual_path = getattr(actual_path, mod) + + # Extract path from module name + actual_path_str = str(actual_path).split("'")[1] + + # Load module with exact path + module = import_module(actual_path_str) + value = module + for attr in attribute.split('.'): + if attr: + value = getattr(value, attr) + + return getattr(value, '__module__', None) except AttributeError: # sphinx.ext.viewcode can't follow class instance attribute # then AttributeError logging output only debug mode. From b332cf7e88837fed744dabdf63a6168ef437b318 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 10:29:10 -0700 Subject: [PATCH 02/32] Add tests --- .../test-ext-viewcode-find-package/conf.py | 24 ++++++++++++++ .../test-ext-viewcode-find-package/index.rst | 10 ++++++ .../main_package/__init__.py | 1 + .../main_package/subpackage/__init__.py | 3 ++ .../subpackage/_subpackage2/__init__.py | 1 + .../subpackage/_subpackage2/submodule.py | 31 +++++++++++++++++++ 6 files changed, 70 insertions(+) create mode 100644 tests/roots/test-ext-viewcode-find-package/conf.py create mode 100644 tests/roots/test-ext-viewcode-find-package/index.rst create mode 100644 tests/roots/test-ext-viewcode-find-package/main_package/__init__.py create mode 100644 tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py create mode 100644 tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/__init__.py create mode 100644 tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py diff --git a/tests/roots/test-ext-viewcode-find-package/conf.py b/tests/roots/test-ext-viewcode-find-package/conf.py new file mode 100644 index 00000000000..6ec2b77429d --- /dev/null +++ b/tests/roots/test-ext-viewcode-find-package/conf.py @@ -0,0 +1,24 @@ +import os +import sys + +source_dir = os.path.abspath('.') +if source_dir not in sys.path: + sys.path.insert(0, source_dir) +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +exclude_patterns = ['_build'] + + +if 'test_linkcode' in tags: # NOQA + extensions.remove('sphinx.ext.viewcode') + extensions.append('sphinx.ext.linkcode') + + def linkcode_resolve(domain, info): + if domain == 'py': + fn = info['module'].replace('.', '/') + return "http://foobar/source/%s.py" % fn + elif domain == "js": + return "http://foobar/js/" + info['fullname'] + elif domain in ("c", "cpp"): + return "http://foobar/%s/%s" % (domain, "".join(info['names'])) + else: + raise AssertionError() diff --git a/tests/roots/test-ext-viewcode-find-package/index.rst b/tests/roots/test-ext-viewcode-find-package/index.rst new file mode 100644 index 00000000000..b40d1cd06c5 --- /dev/null +++ b/tests/roots/test-ext-viewcode-find-package/index.rst @@ -0,0 +1,10 @@ +viewcode +======== + +.. currentmodule:: main_package.subpackage.submodule + +.. autofunction:: func1 + +.. autoclass:: Class1 + +.. autoclass:: Class3 diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py new file mode 100644 index 00000000000..ce7a9deeade --- /dev/null +++ b/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py @@ -0,0 +1 @@ +import main_package.subpackage as subpackage # NOQA diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py new file mode 100644 index 00000000000..e679c3c7f1e --- /dev/null +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py @@ -0,0 +1,3 @@ +from main_package.subpackage._subpackage2 import submodule # NOQA + +__all__ = ["submodule"] \ No newline at end of file diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py new file mode 100644 index 00000000000..8b8e64330ca --- /dev/null +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py @@ -0,0 +1,31 @@ +""" +submodule +""" +# raise RuntimeError('This module should not get imported') + + +def decorator(f): + return f + + +@decorator +def func1(a, b): + """ + this is func1 + """ + return a, b + + +@decorator +class Class1(object): + """ + this is Class1 + """ + + +class Class3(object): + """ + this is Class3 + """ + class_attr = 42 + """this is the class attribute class_attr""" From f6cf7df6701c4e5e4394ee503a6935539f71b740 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 10:31:14 -0700 Subject: [PATCH 03/32] add test --- tests/test_extensions/test_ext_viewcode.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_extensions/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py index eeef391c1e4..ce3a2e37fc6 100644 --- a/tests/test_extensions/test_ext_viewcode.py +++ b/tests/test_extensions/test_ext_viewcode.py @@ -161,3 +161,12 @@ def find_source(app, modname): 'This is the class attribute class_attr', ): assert result.count(needle) == 1 + + +@pytest.mark.sphinx(testroot='ext-viewcode-find-package') +def test_local_source_files(app, status, warning): + app.builder.build_all() + result = (app.outdir / 'index.html').read_text(encoding='utf8') + assert result.count('href="_modules/main_package/subpackage/_subpackage2/submodule.html#func1"') == 1 + assert result.count('href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class1"') == 1 + assert result.count('href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class3"') == 1 From b926d822f99104411f026b426203f2b786f17a59 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 11:51:14 -0700 Subject: [PATCH 04/32] Fix try except issue --- sphinx/ext/viewcode.py | 89 ++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index e697658c505..94ca486e876 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -49,52 +49,55 @@ class viewcode_anchor(Element): def _get_full_modname(modname: str, attribute: str) -> str | None: try: - if modname is None: - # Prevents a TypeError: if the last getattr() call will return None - # then it's better to return it directly - return None - module = import_module(modname) - - # Allow an attribute to have multiple parts and incidentally allow - # repeated .s in the attribute. - value = module - for attr in attribute.split('.'): - if attr: - value = getattr(value, attr) - - return getattr(value, '__module__', None) - except ModuleNotFoundError: - # Attempt to find full path of module - module_path = modname.split('.') - actual_path = __import__(module_path[0], globals(), locals(), [], 0) - if len(module_path) > 1: - for mod in module_path[1:]: - actual_path = getattr(actual_path, mod) - - # Extract path from module name - actual_path_str = str(actual_path).split("'")[1] - - # Load module with exact path - module = import_module(actual_path_str) - value = module - for attr in attribute.split('.'): - if attr: - value = getattr(value, attr) - + try: + if modname is None: + # Prevents a TypeError: if the last getattr() call will return None + # then it's better to return it directly + return None + module = import_module(modname) + + # Allow an attribute to have multiple parts and incidentally allow + # repeated .s in the attribute. + value = module + for attr in attribute.split('.'): + if attr: + value = getattr(value, attr) + + return getattr(value, '__module__', None) + except ModuleNotFoundError: + # Attempt to find full path of module + module_path = modname.split('.') + actual_path = __import__(module_path[0], globals(), locals(), [], 0) + if len(module_path) > 1: + for mod in module_path[1:]: + actual_path = getattr(actual_path, mod) + + # Extract path from module name + actual_path_str = str(actual_path).split("'")[1] + + # Load module with exact path + module = import_module(actual_path_str) + value = module + for attr in attribute.split('.'): + if attr: + value = getattr(value, attr) + return getattr(value, '__module__', None) + raise + except AttributeError: - # sphinx.ext.viewcode can't follow class instance attribute - # then AttributeError logging output only debug mode. - logger.debug("Didn't find %s in %s", attribute, modname) - return None + # sphinx.ext.viewcode can't follow class instance attribute + # then AttributeError logging output only debug mode. + logger.debug("Didn't find %s in %s", attribute, modname) + return None except Exception as e: - # sphinx.ext.viewcode follow python domain directives. - # because of that, if there are no real modules exists that specified - # by py:function or other directives, viewcode emits a lot of warnings. - # It should be displayed only verbose mode. - logger.verbose(traceback.format_exc().rstrip()) - logger.verbose('viewcode can\'t import %s, failed with error "%s"', modname, e) - return None + # sphinx.ext.viewcode follow python domain directives. + # because of that, if there are no real modules exists that specified + # by py:function or other directives, viewcode emits a lot of warnings. + # It should be displayed only verbose mode. + logger.verbose(traceback.format_exc().rstrip()) + logger.verbose('viewcode can\'t import %s, failed with error "%s"', modname, e) + return None def is_supported_builder(builder: Builder) -> bool: From 67c732d8fd4b96f927f8ca7532f36a069c5a83f4 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 11:57:01 -0700 Subject: [PATCH 05/32] Fix linting errors --- sphinx/ext/viewcode.py | 46 ++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 94ca486e876..db00e3b6e68 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -46,7 +46,6 @@ class viewcode_anchor(Element): For not supported builders, they will be removed. """ - def _get_full_modname(modname: str, attribute: str) -> str | None: try: try: @@ -55,49 +54,48 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: # then it's better to return it directly return None module = import_module(modname) - + # Allow an attribute to have multiple parts and incidentally allow # repeated .s in the attribute. value = module - for attr in attribute.split('.'): + for attr in attribute.split("."): if attr: value = getattr(value, attr) - - return getattr(value, '__module__', None) + + return getattr(value, "__module__", None) except ModuleNotFoundError: # Attempt to find full path of module - module_path = modname.split('.') + module_path = modname.split(".") actual_path = __import__(module_path[0], globals(), locals(), [], 0) if len(module_path) > 1: for mod in module_path[1:]: actual_path = getattr(actual_path, mod) - + # Extract path from module name actual_path_str = str(actual_path).split("'")[1] - + # Load module with exact path module = import_module(actual_path_str) value = module - for attr in attribute.split('.'): + for attr in attribute.split("."): if attr: value = getattr(value, attr) - - return getattr(value, '__module__', None) - raise - + + return getattr(value, "__module__", None) + except AttributeError: - # sphinx.ext.viewcode can't follow class instance attribute - # then AttributeError logging output only debug mode. - logger.debug("Didn't find %s in %s", attribute, modname) - return None + # sphinx.ext.viewcode can't follow class instance attribute + # then AttributeError logging output only debug mode. + logger.debug("Didn't find %s in %s", attribute, modname) + return None except Exception as e: - # sphinx.ext.viewcode follow python domain directives. - # because of that, if there are no real modules exists that specified - # by py:function or other directives, viewcode emits a lot of warnings. - # It should be displayed only verbose mode. - logger.verbose(traceback.format_exc().rstrip()) - logger.verbose('viewcode can\'t import %s, failed with error "%s"', modname, e) - return None + # sphinx.ext.viewcode follow python domain directives. + # because of that, if there are no real modules exists that specified + # by py:function or other directives, viewcode emits a lot of warnings. + # It should be displayed only verbose mode. + logger.verbose(traceback.format_exc().rstrip()) + logger.verbose('viewcode can\'t import %s, failed with error "%s"', modname, e) + return None def is_supported_builder(builder: Builder) -> bool: From bf614af2462ef3aca150b98c394d7af0006fc9e7 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 11:57:32 -0700 Subject: [PATCH 06/32] Fix lint --- sphinx/ext/viewcode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index db00e3b6e68..0af164ac4a7 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -46,6 +46,7 @@ class viewcode_anchor(Element): For not supported builders, they will be removed. """ + def _get_full_modname(modname: str, attribute: str) -> str | None: try: try: From bcaec12d8ad7cf201766be77ae9d76b0c8f74793 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 11:59:14 -0700 Subject: [PATCH 07/32] remove old NOQA --- .../test-ext-viewcode-find-package/main_package/__init__.py | 2 +- .../main_package/subpackage/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py index ce7a9deeade..83337e20362 100644 --- a/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py +++ b/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py @@ -1 +1 @@ -import main_package.subpackage as subpackage # NOQA +import main_package.subpackage as subpackage diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py index e679c3c7f1e..d8e0719c061 100644 --- a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py @@ -1,3 +1,3 @@ -from main_package.subpackage._subpackage2 import submodule # NOQA +from main_package.subpackage._subpackage2 import submodule __all__ = ["submodule"] \ No newline at end of file From 487d4ae10b01c6f39d12b1350ebf9dc11bab592a Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 12:01:18 -0700 Subject: [PATCH 08/32] Remove NOQA --- tests/roots/test-ext-viewcode-find-package/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/roots/test-ext-viewcode-find-package/conf.py b/tests/roots/test-ext-viewcode-find-package/conf.py index 6ec2b77429d..bee06398853 100644 --- a/tests/roots/test-ext-viewcode-find-package/conf.py +++ b/tests/roots/test-ext-viewcode-find-package/conf.py @@ -8,7 +8,7 @@ exclude_patterns = ['_build'] -if 'test_linkcode' in tags: # NOQA +if 'test_linkcode' in tags: extensions.remove('sphinx.ext.viewcode') extensions.append('sphinx.ext.linkcode') From 4f2e4016eb6b2daa2109123e3585eebfc01bcef4 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 12:09:23 -0700 Subject: [PATCH 09/32] change test name --- tests/test_extensions/test_ext_viewcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_extensions/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py index ce3a2e37fc6..95c15ebfb96 100644 --- a/tests/test_extensions/test_ext_viewcode.py +++ b/tests/test_extensions/test_ext_viewcode.py @@ -163,8 +163,8 @@ def find_source(app, modname): assert result.count(needle) == 1 -@pytest.mark.sphinx(testroot='ext-viewcode-find-package') -def test_local_source_files(app, status, warning): +@pytest.mark.sphinx('html', testroot='ext-viewcode-find-package', freshenv=True) +def test_find_local_package_import_path(app, status, warning): app.builder.build_all() result = (app.outdir / 'index.html').read_text(encoding='utf8') assert result.count('href="_modules/main_package/subpackage/_subpackage2/submodule.html#func1"') == 1 From 2b998c374d4918aaacfc06c4366428a6fcbeb1ce Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 12:11:00 -0700 Subject: [PATCH 10/32] fix test lint --- tests/test_extensions/test_ext_viewcode.py | 25 +++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/test_extensions/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py index 95c15ebfb96..0d85ac54e07 100644 --- a/tests/test_extensions/test_ext_viewcode.py +++ b/tests/test_extensions/test_ext_viewcode.py @@ -163,10 +163,25 @@ def find_source(app, modname): assert result.count(needle) == 1 -@pytest.mark.sphinx('html', testroot='ext-viewcode-find-package', freshenv=True) +@pytest.mark.sphinx("html", testroot="ext-viewcode-find-package", freshenv=True) def test_find_local_package_import_path(app, status, warning): app.builder.build_all() - result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert result.count('href="_modules/main_package/subpackage/_subpackage2/submodule.html#func1"') == 1 - assert result.count('href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class1"') == 1 - assert result.count('href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class3"') == 1 + result = (app.outdir / "index.html").read_text(encoding="utf8") + assert ( + result.count( + 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#func1"' + ) + == 1 + ) + assert ( + result.count( + 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class1"' + ) + == 1 + ) + assert ( + result.count( + 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class3"' + ) + == 1 + ) From 0ae96b1ca6451033a7a52b12ae4faeef02c95f06 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 12:13:59 -0700 Subject: [PATCH 11/32] fix lint quotation marks --- tests/test_extensions/test_ext_viewcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_extensions/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py index 0d85ac54e07..0c780d81f88 100644 --- a/tests/test_extensions/test_ext_viewcode.py +++ b/tests/test_extensions/test_ext_viewcode.py @@ -163,10 +163,10 @@ def find_source(app, modname): assert result.count(needle) == 1 -@pytest.mark.sphinx("html", testroot="ext-viewcode-find-package", freshenv=True) +@pytest.mark.sphinx('html', testroot='ext-viewcode-find-package', freshenv=True) def test_find_local_package_import_path(app, status, warning): app.builder.build_all() - result = (app.outdir / "index.html").read_text(encoding="utf8") + result = (app.outdir / 'index.html').read_text(encoding='utf8') assert ( result.count( 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#func1"' From 6e3cfcb01130b13d949e25f3d036ce85deaf8f9b Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Thu, 26 Dec 2024 12:18:35 -0700 Subject: [PATCH 12/32] Update authors & changes files --- AUTHORS.rst | 2 +- CHANGES.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 29759d1df7a..e13a81ac0aa 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -28,7 +28,7 @@ Contributors * Antonio Valentino -- qthelp builder, docstring inheritance * Antti Kaihola -- doctest extension (skipif option) * Barry Warsaw -- setup command improvements -* Ben Egan -- Napoleon improvements +* Ben Egan -- Napoleon improvements & Viewcode improvements * Benjamin Peterson -- unittests * Blaise Laflamme -- pyramid theme * Brecht Machiels -- builder entry-points diff --git a/CHANGES.rst b/CHANGES.rst index e20c1e4571e..c8485c4cbec 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,6 +29,8 @@ Bugs fixed * LaTeX: fix a ``7.4.0`` typo in a default for ``\sphinxboxsetup`` (refs: PR #13152). Patch by Jean-François B. +* #13195: viewcode: Fix issue where import paths differ from the directory + structure. Testing ------- From dc4ec10c1ecf10b01cca5e39fec085126697992d Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Sat, 4 Jan 2025 19:05:30 -0700 Subject: [PATCH 13/32] Fix lint errors --- sphinx/ext/viewcode.py | 8 ++++---- tests/roots/test-ext-viewcode-find-package/conf.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 7c8f31690a4..3524f549c95 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -59,14 +59,14 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: # Allow an attribute to have multiple parts and incidentally allow # repeated .s in the attribute. value = module - for attr in attribute.split("."): + for attr in attribute.split('.'): if attr: value = getattr(value, attr) return getattr(value, "__module__", None) except ModuleNotFoundError: # Attempt to find full path of module - module_path = modname.split(".") + module_path = modname.split('.') actual_path = __import__(module_path[0], globals(), locals(), [], 0) if len(module_path) > 1: for mod in module_path[1:]: @@ -78,11 +78,11 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: # Load module with exact path module = import_module(actual_path_str) value = module - for attr in attribute.split("."): + for attr in attribute.split('.'): if attr: value = getattr(value, attr) - return getattr(value, "__module__", None) + return getattr(value, '_module__' None) except AttributeError: # sphinx.ext.viewcode can't follow class instance attribute diff --git a/tests/roots/test-ext-viewcode-find-package/conf.py b/tests/roots/test-ext-viewcode-find-package/conf.py index bee06398853..2350979b602 100644 --- a/tests/roots/test-ext-viewcode-find-package/conf.py +++ b/tests/roots/test-ext-viewcode-find-package/conf.py @@ -16,9 +16,9 @@ def linkcode_resolve(domain, info): if domain == 'py': fn = info['module'].replace('.', '/') return "http://foobar/source/%s.py" % fn - elif domain == "js": - return "http://foobar/js/" + info['fullname'] - elif domain in ("c", "cpp"): - return "http://foobar/%s/%s" % (domain, "".join(info['names'])) + elif domain == 'js': + return 'http://foobar/js/' + info['fullname'] + elif domain in ('c', 'cpp'): + return 'http://foobar/%s/%s' % (domain, ''.join(info['names'])) else: raise AssertionError() From 8c98620db116c85b192ab036e6e353c1dbc37c6f Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Sat, 4 Jan 2025 19:08:17 -0700 Subject: [PATCH 14/32] Update viewcode.py --- sphinx/ext/viewcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 3524f549c95..a86cef19276 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -82,7 +82,7 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: if attr: value = getattr(value, attr) - return getattr(value, '_module__' None) + return getattr(value, '_module__', None) except AttributeError: # sphinx.ext.viewcode can't follow class instance attribute From fd52bc1eb45e1ba8b87a0403b2b5012442a96cd4 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Sat, 4 Jan 2025 19:17:22 -0700 Subject: [PATCH 15/32] Update viewcode.py --- sphinx/ext/viewcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index a86cef19276..a5bd85266e5 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -63,7 +63,7 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: if attr: value = getattr(value, attr) - return getattr(value, "__module__", None) + return getattr(value, '__module__', None) except ModuleNotFoundError: # Attempt to find full path of module module_path = modname.split('.') From abbadbbfc3ae915ac3f4a41ef755a9e3a71f41a5 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Sat, 4 Jan 2025 19:25:57 -0700 Subject: [PATCH 16/32] Fix remaining lint issues --- tests/roots/test-ext-viewcode-find-package/conf.py | 6 +++--- .../test-ext-viewcode-find-package/main_package/__init__.py | 2 +- .../main_package/subpackage/__init__.py | 2 +- .../main_package/subpackage/_subpackage2/submodule.py | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/roots/test-ext-viewcode-find-package/conf.py b/tests/roots/test-ext-viewcode-find-package/conf.py index 2350979b602..d0c0350892c 100644 --- a/tests/roots/test-ext-viewcode-find-package/conf.py +++ b/tests/roots/test-ext-viewcode-find-package/conf.py @@ -15,10 +15,10 @@ def linkcode_resolve(domain, info): if domain == 'py': fn = info['module'].replace('.', '/') - return "http://foobar/source/%s.py" % fn + return 'http://foobar/source/%s.py' % fn elif domain == 'js': return 'http://foobar/js/' + info['fullname'] elif domain in ('c', 'cpp'): - return 'http://foobar/%s/%s' % (domain, ''.join(info['names'])) + return 'http://foobar/%s/%s' % (domain, ''.join(info['names'])) else: - raise AssertionError() + raise AssertionError diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py index 83337e20362..7654a53caa9 100644 --- a/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py +++ b/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py @@ -1 +1 @@ -import main_package.subpackage as subpackage +from main_package import subpackage diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py index d8e0719c061..a1e31add516 100644 --- a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py @@ -1,3 +1,3 @@ from main_package.subpackage._subpackage2 import submodule -__all__ = ["submodule"] \ No newline at end of file +__all__ = ['submodule'] diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py index 8b8e64330ca..1067622727c 100644 --- a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py @@ -27,5 +27,6 @@ class Class3(object): """ this is Class3 """ + class_attr = 42 """this is the class attribute class_attr""" From e91eb2e47bb962cb2d5294c6c577c18d507017db Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Sat, 4 Jan 2025 19:30:53 -0700 Subject: [PATCH 17/32] fix mistake --- sphinx/ext/viewcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index a5bd85266e5..4e6e39312e8 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -82,7 +82,7 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: if attr: value = getattr(value, attr) - return getattr(value, '_module__', None) + return getattr(value, '__module__', None) except AttributeError: # sphinx.ext.viewcode can't follow class instance attribute From bcbccd1cf45834da3584da38b02d70e16de2e8f0 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Sat, 4 Jan 2025 19:37:33 -0700 Subject: [PATCH 18/32] fix remaining lint errors --- tests/roots/test-ext-viewcode-find-package/conf.py | 2 +- .../main_package/subpackage/_subpackage2/submodule.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/roots/test-ext-viewcode-find-package/conf.py b/tests/roots/test-ext-viewcode-find-package/conf.py index d0c0350892c..8c164a272db 100644 --- a/tests/roots/test-ext-viewcode-find-package/conf.py +++ b/tests/roots/test-ext-viewcode-find-package/conf.py @@ -18,7 +18,7 @@ def linkcode_resolve(domain, info): return 'http://foobar/source/%s.py' % fn elif domain == 'js': return 'http://foobar/js/' + info['fullname'] - elif domain in ('c', 'cpp'): + elif domain in {'c', 'cpp'}: return 'http://foobar/%s/%s' % (domain, ''.join(info['names'])) else: raise AssertionError diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py index 1067622727c..ed9445d0f2a 100644 --- a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py @@ -17,13 +17,13 @@ def func1(a, b): @decorator -class Class1(object): +class Class1(): """ this is Class1 """ -class Class3(object): +class Class3(): """ this is Class3 """ From 965cc268b26339d69f87aa96c9c67989f3765992 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Sat, 4 Jan 2025 19:39:22 -0700 Subject: [PATCH 19/32] fix classes --- .../main_package/subpackage/_subpackage2/submodule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py index ed9445d0f2a..a8f8acc8317 100644 --- a/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py @@ -17,13 +17,13 @@ def func1(a, b): @decorator -class Class1(): +class Class1: """ this is Class1 """ -class Class3(): +class Class3: """ this is Class3 """ From 6fa7a43347aeb6e9b1214ed2a53f04b7195b4408 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Sat, 4 Jan 2025 19:43:48 -0700 Subject: [PATCH 20/32] replace __import__ with find_spec --- sphinx/ext/viewcode.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 4e6e39312e8..e4f11b5fb19 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -7,6 +7,7 @@ import posixpath import traceback from importlib import import_module +from importlib.util import find_spec from typing import TYPE_CHECKING, Any, cast from docutils import nodes @@ -67,7 +68,10 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: except ModuleNotFoundError: # Attempt to find full path of module module_path = modname.split('.') - actual_path = __import__(module_path[0], globals(), locals(), [], 0) + module_spec = find_spec(module_path[0]) + if module_spec is None: + return None + actual_path = module_spec.loader.load_module(module_path[0]) if len(module_path) > 1: for mod in module_path[1:]: actual_path = getattr(actual_path, mod) From 87f01b1e2a913ca5608a8919046e2f35292e5c16 Mon Sep 17 00:00:00 2001 From: ProGamerGov Date: Sat, 4 Jan 2025 19:54:20 -0700 Subject: [PATCH 21/32] Fix lint errors --- sphinx/ext/viewcode.py | 2 +- tests/roots/test-ext-viewcode-find-package/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index e4f11b5fb19..cdc7a87044f 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -71,7 +71,7 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: module_spec = find_spec(module_path[0]) if module_spec is None: return None - actual_path = module_spec.loader.load_module(module_path[0]) + actual_path = module_spec.loader.load_module(module_path[0]) # type: ignore[union-attr] if len(module_path) > 1: for mod in module_path[1:]: actual_path = getattr(actual_path, mod) diff --git a/tests/roots/test-ext-viewcode-find-package/conf.py b/tests/roots/test-ext-viewcode-find-package/conf.py index 8c164a272db..cad4c5597de 100644 --- a/tests/roots/test-ext-viewcode-find-package/conf.py +++ b/tests/roots/test-ext-viewcode-find-package/conf.py @@ -8,7 +8,7 @@ exclude_patterns = ['_build'] -if 'test_linkcode' in tags: +if 'test_linkcode' in tags: # NoQA: F821 (tags is injected into conf.py) extensions.remove('sphinx.ext.viewcode') extensions.append('sphinx.ext.linkcode') From a3ff8cb323b510e612b755df85a4500acc4d0995 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 09:47:32 +0000 Subject: [PATCH 22/32] format --- AUTHORS.rst | 2 +- CHANGES.rst | 4 ++-- tests/test_extensions/test_ext_viewcode.py | 27 ++++++++++------------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 600f35ecf34..091a87ebdba 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -28,7 +28,7 @@ Contributors * Antonio Valentino -- qthelp builder, docstring inheritance * Antti Kaihola -- doctest extension (skipif option) * Barry Warsaw -- setup command improvements -* Ben Egan -- Napoleon improvements & Viewcode improvements +* Ben Egan -- Napoleon improvements & viewcode improvements * Benjamin Peterson -- unittests * Blaise Laflamme -- pyramid theme * Brecht Machiels -- builder entry-points diff --git a/CHANGES.rst b/CHANGES.rst index cb4528ecae5..8e4cbfd921c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -41,12 +41,12 @@ Bugs fixed * LaTeX: fix a ``7.4.0`` typo in a default for ``\sphinxboxsetup`` (refs: PR #13152). Patch by Jean-François B. -* #13195: viewcode: Fix issue where import paths differ from the directory - structure. * #13096: HTML Search: check that query terms exist as properties in term indices before accessing them. * #11233: linkcheck: match redirect URIs against :confval:`linkcheck_ignore` by overriding session-level ``requests.get_redirect_target``. +* #13195: viewcode: Fix issue where import paths differ from the directory + structure. Testing ------- diff --git a/tests/test_extensions/test_ext_viewcode.py b/tests/test_extensions/test_ext_viewcode.py index c4126d05f90..5721dfb638f 100644 --- a/tests/test_extensions/test_ext_viewcode.py +++ b/tests/test_extensions/test_ext_viewcode.py @@ -168,21 +168,18 @@ def find_source(app, modname): def test_find_local_package_import_path(app, status, warning): app.builder.build_all() result = (app.outdir / 'index.html').read_text(encoding='utf8') - assert ( - result.count( - 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#func1"' - ) - == 1 + + count_func1 = result.count( + 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#func1"' ) - assert ( - result.count( - 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class1"' - ) - == 1 + assert count_func1 == 1 + + count_class1 = result.count( + 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class1"' ) - assert ( - result.count( - 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class3"' - ) - == 1 + assert count_class1 == 1 + + count_class3 = result.count( + 'href="_modules/main_package/subpackage/_subpackage2/submodule.html#Class3"' ) + assert count_class3 == 1 From e06b21957a808760ade9f02c2780041af3140f47 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 09:47:36 +0000 Subject: [PATCH 23/32] early return --- sphinx/ext/viewcode.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 4b9ec3d82ac..5f3fb651117 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -49,12 +49,13 @@ class viewcode_anchor(Element): def _get_full_modname(modname: str, attribute: str) -> str | None: + if modname is None: + # Prevents a TypeError: if the last getattr() call will return None + # then it's better to return it directly + return None + try: try: - if modname is None: - # Prevents a TypeError: if the last getattr() call will return None - # then it's better to return it directly - return None module = import_module(modname) # Allow an attribute to have multiple parts and incidentally allow From a7d3d1c5b459a7d94bd024a3f44b7d8891fc06bc Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 09:48:54 +0000 Subject: [PATCH 24/32] deduplicate --- sphinx/ext/viewcode.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 5f3fb651117..adabcedfce4 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -57,15 +57,6 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: try: try: module = import_module(modname) - - # Allow an attribute to have multiple parts and incidentally allow - # repeated .s in the attribute. - value = module - for attr in attribute.split('.'): - if attr: - value = getattr(value, attr) - - return getattr(value, '__module__', None) except ModuleNotFoundError: # Attempt to find full path of module module_path = modname.split('.') @@ -82,12 +73,15 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: # Load module with exact path module = import_module(actual_path_str) - value = module - for attr in attribute.split('.'): - if attr: - value = getattr(value, attr) - return getattr(value, '__module__', None) + # Allow an attribute to have multiple parts and incidentally allow + # repeated .s in the attribute. + value = module + for attr in attribute.split('.'): + if attr: + value = getattr(value, attr) + + return getattr(value, '__module__', None) except AttributeError: # sphinx.ext.viewcode can't follow class instance attribute From 7e5dc56cd7bdca9ad8909dadbc90726e06688586 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 09:55:40 +0000 Subject: [PATCH 25/32] Use non-deprecated exec_module --- sphinx/ext/viewcode.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index adabcedfce4..3440cfeba34 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -7,7 +7,7 @@ import posixpath import traceback from importlib import import_module -from importlib.util import find_spec +from importlib.util import find_spec, module_from_spec from typing import TYPE_CHECKING, Any, cast from docutils import nodes @@ -63,13 +63,14 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: module_spec = find_spec(module_path[0]) if module_spec is None: return None - actual_path = module_spec.loader.load_module(module_path[0]) # type: ignore[union-attr] + module = module_from_spec(module_spec) + module_spec.loader.exec_module(module) if len(module_path) > 1: for mod in module_path[1:]: - actual_path = getattr(actual_path, mod) + module = getattr(module, mod) # Extract path from module name - actual_path_str = str(actual_path).split("'")[1] + actual_path_str = str(module).split("'")[1] # Load module with exact path module = import_module(actual_path_str) From 6849c71cfffacedf00d1689e0c365059ec513123 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 09:59:44 +0000 Subject: [PATCH 26/32] Use module.__name__ instead of parsing the module repr --- sphinx/ext/viewcode.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 3440cfeba34..5178513e614 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -69,11 +69,8 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: for mod in module_path[1:]: module = getattr(module, mod) - # Extract path from module name - actual_path_str = str(module).split("'")[1] - # Load module with exact path - module = import_module(actual_path_str) + module = import_module(module.__name__) # Allow an attribute to have multiple parts and incidentally allow # repeated .s in the attribute. From eada6e7db457577b6586b8c528e46847df16d02c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:00:04 +0000 Subject: [PATCH 27/32] module is import_module(module.__name__) --- sphinx/ext/viewcode.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 5178513e614..8920ae90e6c 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -69,9 +69,6 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: for mod in module_path[1:]: module = getattr(module, mod) - # Load module with exact path - module = import_module(module.__name__) - # Allow an attribute to have multiple parts and incidentally allow # repeated .s in the attribute. value = module From 60f7529390df757e258bc8727d05c77ac4b96a01 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:02:02 +0000 Subject: [PATCH 28/32] style --- sphinx/ext/viewcode.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 8920ae90e6c..81807cbae8d 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -59,14 +59,14 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: module = import_module(modname) except ModuleNotFoundError: # Attempt to find full path of module - module_path = modname.split('.') - module_spec = find_spec(module_path[0]) + mod_root, *remaining_mod_path = modname.split('.') + module_spec = find_spec(mod_root) if module_spec is None: return None module = module_from_spec(module_spec) module_spec.loader.exec_module(module) - if len(module_path) > 1: - for mod in module_path[1:]: + if remaining_mod_path: + for mod in remaining_mod_path: module = getattr(module, mod) # Allow an attribute to have multiple parts and incidentally allow From 6e435dd29b04d4c56d15c984831b45699830ef1e Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:02:56 +0000 Subject: [PATCH 29/32] absolute imports --- sphinx/ext/viewcode.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 81807cbae8d..bd4da0e75c4 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -2,12 +2,12 @@ from __future__ import annotations +import importlib.util import operator import os.path import posixpath import traceback from importlib import import_module -from importlib.util import find_spec, module_from_spec from typing import TYPE_CHECKING, Any, cast from docutils import nodes @@ -60,10 +60,10 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: except ModuleNotFoundError: # Attempt to find full path of module mod_root, *remaining_mod_path = modname.split('.') - module_spec = find_spec(mod_root) + module_spec = importlib.util.find_spec(mod_root) if module_spec is None: return None - module = module_from_spec(module_spec) + module = importlib.util.module_from_spec(module_spec) module_spec.loader.exec_module(module) if remaining_mod_path: for mod in remaining_mod_path: @@ -77,7 +77,6 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: value = getattr(value, attr) return getattr(value, '__module__', None) - except AttributeError: # sphinx.ext.viewcode can't follow class instance attribute # then AttributeError logging output only debug mode. From 9dfde4ba94f33f703f3c08aea8c477da08594db9 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:32:26 +0000 Subject: [PATCH 30/32] unify with the 'import_module' approach --- sphinx/ext/viewcode.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index bd4da0e75c4..91ba64e8266 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -55,19 +55,24 @@ def _get_full_modname(modname: str, attribute: str) -> str | None: return None try: - try: - module = import_module(modname) - except ModuleNotFoundError: - # Attempt to find full path of module - mod_root, *remaining_mod_path = modname.split('.') + # Attempt to find full path of module + module_path = modname.split('.') + num_parts = len(module_path) + for i in range(num_parts, 0, -1): + mod_root = '.'.join(module_path[:i]) module_spec = importlib.util.find_spec(mod_root) - if module_spec is None: - return None - module = importlib.util.module_from_spec(module_spec) - module_spec.loader.exec_module(module) - if remaining_mod_path: - for mod in remaining_mod_path: - module = getattr(module, mod) + if module_spec is not None: + break + else: + return None + # Load and execute the module + module = importlib.util.module_from_spec(module_spec) + if module_spec.loader is None: + return None + module_spec.loader.exec_module(module) + if i != num_parts: + for mod in module_path[i:]: + module = getattr(module, mod) # Allow an attribute to have multiple parts and incidentally allow # repeated .s in the attribute. From 6ba6460eeac1a4131f212b1971b09b976dd05d4c Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:35:49 +0000 Subject: [PATCH 31/32] Credit --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 8e4cbfd921c..08b1649e513 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -47,6 +47,7 @@ Bugs fixed overriding session-level ``requests.get_redirect_target``. * #13195: viewcode: Fix issue where import paths differ from the directory structure. + Patch by Ben Egan and Adam Turner. Testing ------- From 69766d1611a56b3174b670cfeb45d4714b19f37d Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 5 Jan 2025 10:36:56 +0000 Subject: [PATCH 32/32] remove unused import --- sphinx/ext/viewcode.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index 91ba64e8266..dc182a91329 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -7,7 +7,6 @@ import os.path import posixpath import traceback -from importlib import import_module from typing import TYPE_CHECKING, Any, cast from docutils import nodes