From: Vladimír Vondruš Date: Sat, 13 Jul 2019 16:30:26 +0000 (+0200) Subject: documentation/python: inspect pybind type __annotations__ carefully. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~cjwatson/git?a=commitdiff_plain;h=c459071e376a44b04a5b298b00d9217f49eb0bd3;p=blog.git documentation/python: inspect pybind type __annotations__ carefully. The typing.get_type_annotations() crashes with a KeyError there because there's no pybind11_builtins module. I don't know and don't want to know what's happening there. --- diff --git a/documentation/python.py b/documentation/python.py index a56de157..91485b18 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -707,7 +707,13 @@ def extract_type(type) -> str: # classes into account. return (type.__module__ + '.' if type.__module__ != 'builtins' else '') + type.__qualname__ -def get_type_hints_or_nothing(path: List[str], object): +def get_type_hints_or_nothing(state: State, path: List[str], object) -> Dict: + # Calling get_type_hints on a pybind11 type (from extract_data_doc()) + # results in KeyError because there's no sys.modules['pybind11_builtins']. + # Be pro-active and return an empty dict if that's the case. + if state.config['PYBIND11_COMPATIBILITY'] and isinstance(object, type) and 'pybind11_builtins' in [a.__module__ for a in object.__mro__]: + return {} + try: return typing.get_type_hints(object) except Exception as e: @@ -945,7 +951,7 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis # converted to actual annotations). If that fails (e.g. because a type # doesn't exist), we'll take the non-dereferenced annotations from # inspect instead. - type_hints = get_type_hints_or_nothing(path, function) + type_hints = get_type_hints_or_nothing(state, path, function) try: signature = inspect.signature(function) @@ -1005,7 +1011,7 @@ def extract_property_doc(state: State, path: List[str], property): # signature because pybind11 properties would throw TypeError from # typing.get_type_hints(). This way they throw ValueError from inspect # and we don't need to handle TypeError in get_type_hints_or_nothing(). - if property.fget: type_hints = get_type_hints_or_nothing(path, property.fget) + if property.fget: type_hints = get_type_hints_or_nothing(state, path, property.fget) if 'return' in type_hints: out.type = extract_annotation(state, path, type_hints['return']) @@ -1033,7 +1039,7 @@ def extract_data_doc(state: State, parent, path: List[str], data): # First try to get fully dereferenced type hints (with strings converted to # actual annotations). If that fails (e.g. because a type doesn't exist), # we'll take the non-dereferenced annotations instead. - type_hints = get_type_hints_or_nothing(path, parent) + type_hints = get_type_hints_or_nothing(state, path, parent) if out.name in type_hints: out.type = extract_annotation(state, path, type_hints[out.name]) diff --git a/documentation/test_python/pybind_type_links/pybind_type_links.Foo.html b/documentation/test_python/pybind_type_links/pybind_type_links.Foo.html index c9ca00ae..baa2bd66 100644 --- a/documentation/test_python/pybind_type_links/pybind_type_links.Foo.html +++ b/documentation/test_python/pybind_type_links/pybind_type_links.Foo.html @@ -31,6 +31,7 @@ @@ -54,6 +55,15 @@
A property
+
+

Data

+
+
+ TYPE_DATA: Enum = Enum.SECOND +
+
+
+
diff --git a/documentation/test_python/pybind_type_links/pybind_type_links.cpp b/documentation/test_python/pybind_type_links/pybind_type_links.cpp index 54d0c432..f6b3f8e6 100644 --- a/documentation/test_python/pybind_type_links/pybind_type_links.cpp +++ b/documentation/test_python/pybind_type_links/pybind_type_links.cpp @@ -28,7 +28,8 @@ PYBIND11_MODULE(pybind_type_links, m) { .value("FIRST", Enum::First) .value("SECOND", Enum::Second); - py::class_{m, "Foo", "A class"} + py::class_ foo{m, "Foo", "A class"}; + foo .def(py::init(), "Constructor") .def_readwrite("property", &Foo::property, "A property"); @@ -36,4 +37,8 @@ PYBIND11_MODULE(pybind_type_links, m) { .def("type_enum", &typeEnum, "A function taking an enum") .def("type_return", &typeReturn, "A function returning a type") .def("type_nested", &typeNested, "A function with nested type annotation"); + + /* Test also attributes (annotated from within Python) */ + m.attr("TYPE_DATA") = Foo{Enum::First}; + foo.attr("TYPE_DATA") = Enum::Second; } diff --git a/documentation/test_python/pybind_type_links/pybind_type_links.html b/documentation/test_python/pybind_type_links/pybind_type_links.html index 85e6e06e..3cf47e7a 100644 --- a/documentation/test_python/pybind_type_links/pybind_type_links.html +++ b/documentation/test_python/pybind_type_links/pybind_type_links.html @@ -32,6 +32,7 @@
  • Classes
  • Enums
  • Functions
  • +
  • Data
  • @@ -70,6 +71,15 @@
    A function returning a type
    +
    +

    Data

    +
    +
    + TYPE_DATA: Foo +
    +
    +
    +
    diff --git a/documentation/test_python/test_pybind.py b/documentation/test_python/test_pybind.py index 098c9de4..b4fd37ad 100644 --- a/documentation/test_python/test_pybind.py +++ b/documentation/test_python/test_pybind.py @@ -239,7 +239,15 @@ class TypeLinks(BaseInspectTestCase): super().__init__(__file__, 'type_links', *args, **kwargs) def test(self): + sys.path.append(self.path) + import pybind_type_links + # Annotate the type of TYPE_DATA (TODO: can this be done from pybind?) + pybind_type_links.__annotations__ = {} + pybind_type_links.__annotations__['TYPE_DATA'] = pybind_type_links.Foo + pybind_type_links.Foo.__annotations__ = {} + pybind_type_links.Foo.__annotations__['TYPE_DATA'] = pybind_type_links.Enum self.run_python({ + 'INPUT_MODULES': [pybind_type_links], 'PYBIND11_COMPATIBILITY': True }) self.assertEqual(*self.actual_expected_contents('pybind_type_links.html'))