diff --git a/src/sphinx_autodoc_typehints/__init__.py b/src/sphinx_autodoc_typehints/__init__.py index 6002019..9687626 100644 --- a/src/sphinx_autodoc_typehints/__init__.py +++ b/src/sphinx_autodoc_typehints/__init__.py @@ -169,6 +169,18 @@ def format_internal_tuple(t: tuple[Any, ...], config: Config) -> str: return f"({', '.join(fmt)})" +def fixup_module_name(config: Config, module: str) -> str: + if config.typehints_fixup_module_name: + module = config.typehints_fixup_module_name(module) + + if module == "typing_extensions": + module = "typing" + + if module == "_io": + module = "io" + return module + + def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PLR0911, PLR0912, PLR0915, PLR0914 """ Format the annotation. @@ -204,13 +216,7 @@ def format_annotation(annotation: Any, config: Config) -> str: # noqa: C901, PL except ValueError: return str(annotation).strip("'") - # Redirect all typing_extensions types to the stdlib typing module - if module == "typing_extensions": - module = "typing" - - if module == "_io": - module = "io" - + module = fixup_module_name(config, module) full_name = f"{module}.{class_name}" if module != "builtins" else class_name fully_qualified: bool = getattr(config, "typehints_fully_qualified", False) prefix = "" if fully_qualified or full_name == class_name else "~" @@ -967,6 +973,7 @@ def setup(app: Sphinx) -> dict[str, bool]: app.add_config_value("typehints_formatter", None, "env") app.add_config_value("typehints_use_signature", False, "env") # noqa: FBT003 app.add_config_value("typehints_use_signature_return", False, "env") # noqa: FBT003 + app.add_config_value("typehints_fixup_module_name", None, "env") app.add_role("sphinx_autodoc_typehints_type", sphinx_autodoc_typehints_type_role) app.connect("env-before-read-docs", validate_config) # config may be changed after “config-inited” event app.connect("autodoc-process-signature", process_signature) diff --git a/tests/roots/test-dummy/export_module.py b/tests/roots/test-dummy/export_module.py new file mode 100644 index 0000000..0fa35a8 --- /dev/null +++ b/tests/roots/test-dummy/export_module.py @@ -0,0 +1,5 @@ +from __future__ import annotations + +from wrong_module_path import A, f + +__all__ = ["A", "f"] diff --git a/tests/roots/test-dummy/wrong_module_path.py b/tests/roots/test-dummy/wrong_module_path.py new file mode 100644 index 0000000..e5542ed --- /dev/null +++ b/tests/roots/test-dummy/wrong_module_path.py @@ -0,0 +1,9 @@ +from __future__ import annotations + + +class A: + pass + + +def f() -> A: + pass diff --git a/tests/roots/test-dummy/wrong_module_path.rst b/tests/roots/test-dummy/wrong_module_path.rst new file mode 100644 index 0000000..1919331 --- /dev/null +++ b/tests/roots/test-dummy/wrong_module_path.rst @@ -0,0 +1,3 @@ +.. class:: export_module.A + +.. autofunction:: export_module.f diff --git a/tests/test_integration.py b/tests/test_integration.py index 8d732d9..f738053 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1373,6 +1373,7 @@ def test_integration( (Path(app.srcdir) / "index.rst").write_text(template.format(val.__name__)) app.config.__dict__.update(configs[conf_run]) app.config.__dict__.update(val.OPTIONS) + app.config.nitpicky = True monkeypatch.setitem(sys.modules, "mod", sys.modules[__name__]) app.build() assert "build succeeded" in status.getvalue() # Build succeeded diff --git a/tests/test_sphinx_autodoc_typehints.py b/tests/test_sphinx_autodoc_typehints.py index 93df0fe..f6aa487 100644 --- a/tests/test_sphinx_autodoc_typehints.py +++ b/tests/test_sphinx_autodoc_typehints.py @@ -1123,3 +1123,25 @@ def test_default_annotation_without_typehints(app: SphinxTestApp, status: String "str" """ assert text_contents == dedent(expected_contents) + + +@pytest.mark.sphinx("text", testroot="dummy") +@patch("sphinx.writers.text.MAXWIDTH", 2000) +def test_wrong_module_path(app: SphinxTestApp, status: StringIO, warning: StringIO) -> None: + set_python_path() + + app.config.master_doc = "wrong_module_path" # create flag + app.config.default_role = "literal" + app.config.nitpicky = True + + def fixup_module_name(mod: str) -> str: + if not mod.startswith("wrong_module_path"): + return mod + return "export_module" + mod.removeprefix("wrong_module_path") + + app.config.suppress_warnings = ["config.cache"] + app.config.typehints_fixup_module_name = fixup_module_name + app.build() + + assert "build succeeded" in status.getvalue() # Build succeeded + assert not warning.getvalue().strip()