diff --git a/AUTHORS b/AUTHORS index a35c27044f0..af9f43ac3ff 100644 --- a/AUTHORS +++ b/AUTHORS @@ -30,7 +30,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 & Viewcode improvements * Benjamin Peterson -- unittests * Blaise Laflamme -- pyramid theme * Bruce Mitchener -- Minor epub improvement diff --git a/CHANGES b/CHANGES index df90f538824..01b414b57e9 100644 --- a/CHANGES +++ b/CHANGES @@ -46,6 +46,8 @@ Features added * #10738: napoleon: Add support for docstring types using 'of', like ``type of type``. Example: ``tuple of int``. +* #10766: viewcode: Support import paths that differ from the directory + structure. Bugs fixed ---------- diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index 007a2bf5d28..4f39784cd20 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -254,7 +254,22 @@ def get_full_modname(modname: str, attribute: str) -> Optional[str]: # 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) + + try: + module = import_module(modname) + 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) # Allow an attribute to have multiple parts and incidentally allow # repeated .s in the attribute. 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..bee06398853 --- /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: + 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..8619564f930 --- /dev/null +++ b/tests/roots/test-ext-viewcode-find-package/main_package/__init__.py @@ -0,0 +1 @@ +import main_package.subpackage as subpackage # noqa: F401 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..53e7c28ad41 --- /dev/null +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/__init__.py @@ -0,0 +1 @@ +from main_package.subpackage._subpackage2 import submodule # noqa: F401 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..39b00843b66 --- /dev/null +++ b/tests/roots/test-ext-viewcode-find-package/main_package/subpackage/_subpackage2/submodule.py @@ -0,0 +1,30 @@ +""" +submodule +""" + + +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""" diff --git a/tests/test_ext_viewcode.py b/tests/test_ext_viewcode.py index 7750b8da055..94755644cae 100644 --- a/tests/test_ext_viewcode.py +++ b/tests/test_ext_viewcode.py @@ -115,3 +115,12 @@ def find_source(app, modname): assert result.count('href="_modules/not_a_package/submodule.html#not_a_package.submodule.Class3.class_attr"') == 1 assert result.count('This is the class attribute class_attr') == 1 + + +@pytest.mark.sphinx(testroot='ext-viewcode-find-package') +def test_viewcode_shortened_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