diff --git a/CHANGELOG.md b/CHANGELOG.md index d64bb2a6..68cff0a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## Unreleased: pdoc next +- Fix a crash when a `type` alias references a name that is only imported in an `if TYPE_CHECKING:` block. + ([#900](https://github.com/mitmproxy/pdoc/pull/900), @cristianchiriac) - Fix incorrect ordering of `start-after`/`end-before` in RST include directives. ([#876](https://github.com/mitmproxy/pdoc/pull/876), @JohanKarlbergg) - Support Pydantic [`computed_field`](https://docs.pydantic.dev/2.0/usage/computed_fields/) descriptions ([#855](https://github.com/mitmproxy/pdoc/pull/855), @avhz) diff --git a/pdoc/doc.py b/pdoc/doc.py index 41c0d673..3318f009 100644 --- a/pdoc/doc.py +++ b/pdoc/doc.py @@ -1143,7 +1143,20 @@ def default_value_str(self) -> str: if self.default_value is empty: return "" if isinstance(self.default_value, TypeAliasType): - formatted = formatannotation(self.default_value.__value__) + try: + value = self.default_value.__value__ + except NameError: + module_name = self.default_value.__module__ + module = sys.modules.get(module_name) if module_name else None + if module is None: + return self.default_value.__name__ + expression = f"{self.default_value.__name__}.__value__" + value = safe_eval_type( + expression, module.__dict__, None, module, self.fullname + ) + if isinstance(value, str) and value == expression: + return self.default_value.__name__ + formatted = formatannotation(value) return _remove_collections_abc(formatted) elif self.annotation == TypeAlias: formatted = formatannotation(self.default_value) diff --git a/test/test_doc.py b/test/test_doc.py index 3fcf96d4..50022fbb 100644 --- a/test/test_doc.py +++ b/test/test_doc.py @@ -1,11 +1,13 @@ import builtins import dataclasses from pathlib import Path +import sys import types from unittest.mock import patch import pytest +import pdoc from pdoc import extract from pdoc.doc import Class from pdoc.doc import Module @@ -16,6 +18,43 @@ here = Path(__file__).parent +@pytest.mark.skipif(sys.version_info < (3, 12), reason="requires type statements") +def test_type_alias_with_type_checking_import(tmp_path): + module_file = tmp_path / "alias_with_type_checking.py" + module_file.write_text( + "from __future__ import annotations\n" + "import typing\n" + "if typing.TYPE_CHECKING:\n" + " from collections.abc import Sequence\n" + "type Values = Sequence[int]\n" + ) + mod = extract.load_module(extract.parse_spec(module_file)) + alias = Module(mod).members["Values"] + assert isinstance(alias, Variable) + assert alias.default_value_str == "Sequence[int]" + assert "Sequence[int]" in pdoc.pdoc(str(module_file)) + + +@pytest.mark.skipif(sys.version_info < (3, 12), reason="requires type statements") +def test_type_alias_with_unresolvable_value(tmp_path): + module_file = tmp_path / "unresolvable_alias.py" + module_file.write_text("type Missing = DoesNotExist\n") + mod = extract.load_module(extract.parse_spec(module_file)) + with pytest.warns(UserWarning, match="Error parsing type annotation"): + assert Module(mod).members["Missing"].default_value_str == "Missing" + + namespace = {"__name__": "unregistered_module"} + exec("type Hidden = DoesNotExist", namespace) + alias = Variable( + "unregistered_module", + "Hidden", + taken_from=("unregistered_module", "Hidden"), + docstring="", + default_value=namespace["Hidden"], + ) + assert alias.default_value_str == "Hidden" + + def test_repr_tb(monkeypatch): m = Module(dataclasses) with patch("pdoc.doc._docstr", side_effect=ValueError):