diff --git a/pdoc/__init__.py b/pdoc/__init__.py index 8d8c3b09..90fc0d5b 100644 --- a/pdoc/__init__.py +++ b/pdoc/__init__.py @@ -1403,6 +1403,19 @@ def maybe_replace_reprs(a): return str(inspect.formatannotation(maybe_replace_reprs(annot))) +class _PermissiveFallback: + """ + Returns `Any` for missing attributes (used for annotation-only + modules where the installed version is missingm newer names, e.g. + `typing_extensions.CapsuleType` on Python < 3.13). + """ + def __init__(self, mod): + self._mod = mod + + def __getattr__(self, name): + return getattr(self._mod, name, Any) + + class Function(Doc): """ Representation of documentation for a function or method. @@ -1648,9 +1661,30 @@ def _signature_from_string(self): if strings: string = filter(strings[0]) _locals, _globals = {}, {} - _globals.update({'capsule': None}) # pybind11 capsule data type + _globals.update({'capsule': Any}) # pybind11 capsule data type + + # pybind11 >= 3.0.0 uses typing.SupportsInt, etc. + _globals['typing'] = typing _globals.update(typing.__dict__) + + # types is optional and may lack newer names + # (e.g. types.CapsuleType) depending on installed version + try: + import types + _globals['types'] = _PermissiveFallback(types) + except ImportError: + pass + + # typing_extensions is optional and may lack newer names + # (e.g. typing_extensions.CapsuleType) depending on installed version + try: + import typing_extensions + _globals['typing_extensions'] = _PermissiveFallback(typing_extensions) + except ImportError: + pass + _globals.update(self.module.obj.__dict__) + # Trim binding module basename from type annotations # See: https://github.com/pdoc3/pdoc/pull/148#discussion_r407114141 module_basename = self.module.name.rsplit('.', maxsplit=1)[-1] diff --git a/pdoc/test/__init__.py b/pdoc/test/__init__.py index bc4a3a6f..cec6c0f7 100644 --- a/pdoc/test/__init__.py +++ b/pdoc/test/__init__.py @@ -3,6 +3,7 @@ """ import doctest import enum +import importlib.util import inspect import os import shutil @@ -104,6 +105,13 @@ def wrapper(*args, **kwargs): return wrapper +def has_typing_extensions_capsule_type(): + if importlib.util.find_spec('typing_extensions') is None: + return False + import typing_extensions + return hasattr(typing_extensions, 'CapsuleType') + + class CliTest(unittest.TestCase): """ Command-line interface unit tests. @@ -1018,6 +1026,94 @@ def f() -> typing.List[typing.Union[str, pdoc.Doc]]: return [] # noqa: E704 func = pdoc.Function('f', DUMMY_PDOC_MODULE, f) self.assertEqual(func.return_annotation(), 'List[str\N{NBSP}|\N{NBSP}pdoc.Doc]') + def test_Function_signature_from_string(self): + fake_module = ModuleType('fake_pybind11_module') + # sanity check the isolation itself + assert 'typing' not in fake_module.__dict__ + mod = pdoc.Module(fake_module) + + # Union types with `|` operator where introduces with Python 3.10 + if sys.version_info >= (3, 10): + class pybind11_int: + """ + pybind11_int(self: str, pos: typing.SupportsInt | typing.SupportsIndex) -> \ + tuple[int, float] + """ + func = pdoc.Function('pybind11_int', mod, pybind11_int) + sig = pdoc.Function._signature_from_string(func) + + def expected_pybind11_int( + self: str, pos: typing.SupportsInt | typing.SupportsIndex) \ + -> tuple[int, float]: + raise NotImplementedError + self.assertEqual(sig, inspect.signature(expected_pybind11_int)) + + class pybind11_float: + """ + pybind11_float(self: str, pos: typing.SupportsFloat | typing.SupportsIndex) -> \ + tuple[int, float] + """ + func = pdoc.Function('pybind11_float', mod, pybind11_float) + sig = pdoc.Function._signature_from_string(func) + + def expected_pybind11_float( + self: str, pos: typing.SupportsFloat | typing.SupportsIndex) \ + -> tuple[int, float]: + raise NotImplementedError + self.assertEqual(sig, inspect.signature(expected_pybind11_float)) + else: + class pybind11_int: + """ + pybind11_int(self: str, pos: typing.SupportsInt) -> tuple[int, float] + """ + func = pdoc.Function('pybind11_int', mod, pybind11_int) + sig = pdoc.Function._signature_from_string(func) + + def expected_pybind11_int(self: str, pos: typing.SupportsInt) -> tuple[int, float]: + raise NotImplementedError + self.assertEqual(sig, inspect.signature(expected_pybind11_int)) + + class pybind11_float: + """ + pybind11_float(self: str, pos: typing.SupportsFloat) -> tuple[int, float] + """ + func = pdoc.Function('pybind11_float', mod, pybind11_float) + sig = pdoc.Function._signature_from_string(func) + + def expected_pybind11_float(self: str, pos: typing.SupportsFloat) -> tuple[int, float]: + raise NotImplementedError + self.assertEqual(sig, inspect.signature(expected_pybind11_float)) + + if sys.version_info >= (3, 13): + import types + + class pybind11_capsule: + """ + pybind11_capsule(self: str, pointer: types.CapsuleType) -> tuple[int, float] + """ + func = pdoc.Function('pybind11_capsule', mod, pybind11_capsule) + sig = pdoc.Function._signature_from_string(func) + + def expected_pybind11_capsule( + self: str, pointer: types.CapsuleType) -> tuple[int, float]: + raise NotImplementedError + self.assertEqual(sig, inspect.signature(expected_pybind11_capsule)) + elif has_typing_extensions_capsule_type(): + import typing_extensions + + class pybind11_capsule: + """ + pybind11_capsule(self: str, pointer: typing_extensions.CapsuleType) -> \ + tuple[int, float] + """ + func = pdoc.Function('pybind11_capsule', mod, pybind11_capsule) + sig = pdoc.Function._signature_from_string(func) + + def expected_pybind11_capsule( + self: str, pointer: typing_extensions.CapsuleType) -> tuple[int, float]: + raise NotImplementedError + self.assertEqual(sig, inspect.signature(expected_pybind11_capsule)) + @ignore_warnings def test_Variable_type_annotation(self): class Foobar: