From 14c2dac74d141e4909ff10934506ff906f4c6b72 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 14 Jul 2019 17:49:39 +0200 Subject: [PATCH] documentation/python: properly handle inspection of slot properties. There's no fget/fset/fdel for those and we also can't __doc__ them. --- documentation/python.py | 34 +++++++++- .../inspect_annotations.FooSlots.html | 55 +++++++++++++++ .../inspect_annotations.html | 2 + .../inspect_annotations.py | 7 ++ .../test_python/inspect_string/classes.html | 1 + .../inspect_string.FooSlots.html | 67 +++++++++++++++++++ .../inspect_string/inspect_string.html | 2 + .../inspect_string/inspect_string/__init__.py | 5 ++ .../inspect_type_links.second.FooSlots.html | 55 +++++++++++++++ ...ect_type_links.second.FooSlotsInvalid.html | 51 ++++++++++++++ .../inspect_type_links.second.html | 4 ++ .../inspect_type_links/second.py | 15 +++++ documentation/test_python/test_inspect.py | 5 ++ 13 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 documentation/test_python/inspect_annotations/inspect_annotations.FooSlots.html create mode 100644 documentation/test_python/inspect_string/inspect_string.FooSlots.html create mode 100644 documentation/test_python/inspect_type_links/inspect_type_links.second.FooSlots.html create mode 100644 documentation/test_python/inspect_type_links/inspect_type_links.second.FooSlotsInvalid.html diff --git a/documentation/python.py b/documentation/python.py index 84df56e6..602d7089 100755 --- a/documentation/python.py +++ b/documentation/python.py @@ -1085,12 +1085,42 @@ def extract_function_doc(state: State, parent, path: List[str], function) -> Lis return [out] -def extract_property_doc(state: State, path: List[str], property): +def extract_property_doc(state: State, parent, path: List[str], property): assert inspect.isdatadescriptor(property) out = Empty() out.name = path[-1] out.id = state.config['ID_FORMATTER'](EntryType.PROPERTY, path[-1:]) + + # If this is a slot, there won't be any fget / fset / fdel. Assume they're + # gettable and settable (couldn't find any way to make them *inspectably* + # readonly, all solutions involved throwing from __setattr__()) and + # deletable as well (calling del on it seems to simply remove any + # previously set value). Unfortunately we can't get any docstring for these + # either. + # TODO: any better way to detect that those are slots? + if property.__class__.__name__ == 'member_descriptor' and property.__class__.__module__ == 'builtins': + out.is_gettable = True + out.is_settable = True + out.is_deletable = True + # TODO: external summary for properties + out.summary = '' + out.has_details = False + + # 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(state, path, parent) + + if out.name in type_hints: + out.type = extract_annotation(state, path, type_hints[out.name]) + elif hasattr(parent, '__annotations__') and out.name in parent.__annotations__: + out.type = extract_annotation(state, path, parent.__annotations__[out.name]) + else: + out.type = None + + return out + # TODO: external summary for properties out.is_gettable = property.fget is not None if property.fget or (property.fset and property.__doc__): @@ -1334,7 +1364,7 @@ def render_class(state: State, path, class_, env): else: page.methods += [function] elif member_entry.type == EntryType.PROPERTY: - page.properties += [extract_property_doc(state, subpath, member_entry.object)] + page.properties += [extract_property_doc(state, class_, subpath, member_entry.object)] elif member_entry.type == EntryType.DATA: page.data += [extract_data_doc(state, class_, subpath, member_entry.object)] else: # pragma: no cover diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.FooSlots.html b/documentation/test_python/inspect_annotations/inspect_annotations.FooSlots.html new file mode 100644 index 00000000..6273b60b --- /dev/null +++ b/documentation/test_python/inspect_annotations/inspect_annotations.FooSlots.html @@ -0,0 +1,55 @@ + + + + + inspect_annotations.FooSlots | My Python Project + + + + + +
+
+ + diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.html b/documentation/test_python/inspect_annotations/inspect_annotations.html index cc80ecf1..50f2489c 100644 --- a/documentation/test_python/inspect_annotations/inspect_annotations.html +++ b/documentation/test_python/inspect_annotations/inspect_annotations.html @@ -41,6 +41,8 @@
class Foo
A class with properties
+
class FooSlots
+
A class with slots
diff --git a/documentation/test_python/inspect_annotations/inspect_annotations.py b/documentation/test_python/inspect_annotations/inspect_annotations.py index 27e8a3da..65594737 100644 --- a/documentation/test_python/inspect_annotations/inspect_annotations.py +++ b/documentation/test_python/inspect_annotations/inspect_annotations.py @@ -12,6 +12,13 @@ class Foo: """A property with a type annotation""" pass +class FooSlots: + """A class with slots""" + + __slots__ = ['unannotated', 'annotated'] + + annotated: List[str] + def annotation(param: List[int], another: bool, third: str = "hello") -> float: """An annotated function""" pass diff --git a/documentation/test_python/inspect_string/classes.html b/documentation/test_python/inspect_string/classes.html index 1b28b235..617190ca 100644 --- a/documentation/test_python/inspect_string/classes.html +++ b/documentation/test_python/inspect_string/classes.html @@ -49,6 +49,7 @@
  • class Subclass A subclass of Foo
  • +
  • class FooSlots A class with slots. Can't have docstrings for these.
  • class Specials Special class members
  • diff --git a/documentation/test_python/inspect_string/inspect_string.FooSlots.html b/documentation/test_python/inspect_string/inspect_string.FooSlots.html new file mode 100644 index 00000000..02f30ee9 --- /dev/null +++ b/documentation/test_python/inspect_string/inspect_string.FooSlots.html @@ -0,0 +1,67 @@ + + + + + inspect_string.FooSlots | My Python Project + + + + + +
    +
    +
    +
    +
    +

    + inspect_string.FooSlots class +

    +

    A class with slots. Can't have docstrings for these.

    +
    +

    Contents

    + +
    +
    +

    Properties

    +
    +
    + first get set del +
    +
    +
    + second get set del +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/documentation/test_python/inspect_string/inspect_string.html b/documentation/test_python/inspect_string/inspect_string.html index 17fc86dc..5b7cf970 100644 --- a/documentation/test_python/inspect_string/inspect_string.html +++ b/documentation/test_python/inspect_string/inspect_string.html @@ -64,6 +64,8 @@
    class Foo
    The foo class
    +
    class FooSlots
    +
    A class with slots. Can't have docstrings for these.
    class Specials
    Special class members
    diff --git a/documentation/test_python/inspect_string/inspect_string/__init__.py b/documentation/test_python/inspect_string/inspect_string/__init__.py index 425511bc..e9034255 100644 --- a/documentation/test_python/inspect_string/inspect_string/__init__.py +++ b/documentation/test_python/inspect_string/inspect_string/__init__.py @@ -107,6 +107,11 @@ class Foo: """A private property""" pass +class FooSlots: + """A class with slots. Can't have docstrings for these.""" + + __slots__ = ['first', 'second'] + class Specials: """Special class members""" diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.second.FooSlots.html b/documentation/test_python/inspect_type_links/inspect_type_links.second.FooSlots.html new file mode 100644 index 00000000..1c79b018 --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.second.FooSlots.html @@ -0,0 +1,55 @@ + + + + + inspect_type_links.second.FooSlots | My Python Project + + + + + +
    +
    + + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.second.FooSlotsInvalid.html b/documentation/test_python/inspect_type_links/inspect_type_links.second.FooSlotsInvalid.html new file mode 100644 index 00000000..73b24346 --- /dev/null +++ b/documentation/test_python/inspect_type_links/inspect_type_links.second.FooSlotsInvalid.html @@ -0,0 +1,51 @@ + + + + + inspect_type_links.second.FooSlotsInvalid | My Python Project + + + + + +
    +
    +
    +
    +
    +

    + inspect_type_links.second.FooSlotsInvalid class +

    +

    A slot class with an invalid annotation. Has to be separate because otherwise it would invalidate all other slot annotations in FooSlots as well.

    +
    +

    Contents

    + +
    +
    +

    Properties

    +
    +
    + type_slot_string_invalid: typing.List[FooBar] get set del +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/documentation/test_python/inspect_type_links/inspect_type_links.second.html b/documentation/test_python/inspect_type_links/inspect_type_links.second.html index cd07de9e..368458f3 100644 --- a/documentation/test_python/inspect_type_links/inspect_type_links.second.html +++ b/documentation/test_python/inspect_type_links/inspect_type_links.second.html @@ -42,6 +42,10 @@
    class Foo
    A class in the second module
    +
    class FooSlots
    +
    A slot class
    +
    class FooSlotsInvalid
    +
    A slot class with an invalid annotation. Has to be separate because otherwise it would invalidate all other slot annotations in FooSlots as well.
    diff --git a/documentation/test_python/inspect_type_links/inspect_type_links/second.py b/documentation/test_python/inspect_type_links/inspect_type_links/second.py index fbcad3e5..e4f00baf 100644 --- a/documentation/test_python/inspect_type_links/inspect_type_links/second.py +++ b/documentation/test_python/inspect_type_links/inspect_type_links/second.py @@ -44,6 +44,21 @@ class Foo: # other data annotations from being retrieved TYPE_DATA_STRING_INVALID: 'Foo.Bar' = 3 +class FooSlots: + """A slot class""" + + __slots__ = ['type_slot', 'type_slot_string_nested'] + + type_slot: Enum + type_slot_string_nested: 'Tuple[Foo, List[Enum], Any]' + +class FooSlotsInvalid: + """A slot class with an invalid annotation. Has to be separate because otherwise it would invalidate all other slot annotations in FooSlots as well.""" + + __slots__ = ['type_slot_string_invalid'] + + type_slot_string_invalid: List['FooBar'] + def type_string(a: 'Foo'): """A function with string type annotation""" diff --git a/documentation/test_python/test_inspect.py b/documentation/test_python/test_inspect.py index c6d76791..25c0d75f 100644 --- a/documentation/test_python/test_inspect.py +++ b/documentation/test_python/test_inspect.py @@ -43,6 +43,7 @@ class String(BaseInspectTestCase): self.assertEqual(*self.actual_expected_contents('inspect_string.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.another_module.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.Foo.html')) + self.assertEqual(*self.actual_expected_contents('inspect_string.FooSlots.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.Specials.html')) self.assertEqual(*self.actual_expected_contents('classes.html')) @@ -65,6 +66,7 @@ class Object(BaseInspectTestCase): self.assertEqual(*self.actual_expected_contents('inspect_string.html', '../inspect_string/inspect_string.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.another_module.html', '../inspect_string/inspect_string.another_module.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.Foo.html', '../inspect_string/inspect_string.Foo.html')) + self.assertEqual(*self.actual_expected_contents('inspect_string.FooSlots.html', '../inspect_string/inspect_string.FooSlots.html')) self.assertEqual(*self.actual_expected_contents('inspect_string.Specials.html', '../inspect_string/inspect_string.Specials.html')) self.assertEqual(*self.actual_expected_contents('classes.html', '../inspect_string/classes.html')) @@ -80,6 +82,7 @@ class Annotations(BaseInspectTestCase): self.run_python() self.assertEqual(*self.actual_expected_contents('inspect_annotations.html')) self.assertEqual(*self.actual_expected_contents('inspect_annotations.Foo.html')) + self.assertEqual(*self.actual_expected_contents('inspect_annotations.FooSlots.html')) @unittest.skipUnless(LooseVersion(sys.version) >= LooseVersion('3.7'), "signature with / for pow() is not present in 3.6") @@ -140,3 +143,5 @@ class TypeLinks(BaseInspectTestCase): self.assertEqual(*self.actual_expected_contents('inspect_type_links.second.html')) self.assertEqual(*self.actual_expected_contents('inspect_type_links.second.Foo.html')) + self.assertEqual(*self.actual_expected_contents('inspect_type_links.second.FooSlots.html')) + self.assertEqual(*self.actual_expected_contents('inspect_type_links.second.FooSlotsInvalid.html')) -- 2.30.2