From e52eb0be3cfd7437a82dd3aa9293d71527fc695c Mon Sep 17 00:00:00 2001 From: Joel Gibson Date: Thu, 22 Apr 2021 15:40:49 +1200 Subject: [PATCH] feat: Allow method_descriptors to be serialized as methods --- src/pytkdocs/loader.py | 30 +++++++++++++++++++++++++++-- tests/fixtures/method_descriptor.py | 3 +++ tests/test_loader.py | 11 +++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/method_descriptor.py diff --git a/src/pytkdocs/loader.py b/src/pytkdocs/loader.py index 0940554..cf86ffa 100644 --- a/src/pytkdocs/loader.py +++ b/src/pytkdocs/loader.py @@ -173,6 +173,18 @@ def is_method(self) -> bool: function_type = type(lambda: None) return self.parent_is_class() and isinstance(self.obj, function_type) + def is_method_descriptor(self) -> bool: + """ + Tell if this node's object is a method descriptor. + + Built-in methods (e.g. those implemented in C/Rust) are often + method descriptors, rather than normal methods. + + Returns: + If this node's object is a method descriptor. + """ + return inspect.ismethoddescriptor(self.obj) + def is_staticmethod(self) -> bool: """ Tell if this node's object is a staticmethod. @@ -360,6 +372,8 @@ def get_object_documentation(self, dotted_path: str, members: Optional[Union[Set root_object = self.get_staticmethod_documentation(leaf) elif leaf.is_classmethod(): root_object = self.get_classmethod_documentation(leaf) + elif leaf.is_method_descriptor(): + root_object = self.get_regular_method_documentation(leaf) elif leaf.is_method(): root_object = self.get_regular_method_documentation(leaf) elif leaf.is_function(): @@ -792,7 +806,7 @@ def get_regular_method_documentation(self, node: ObjectNode) -> Method: def get_method_documentation(self, node: ObjectNode, properties: Optional[List[str]] = None) -> Method: """ - Get the documentation for a method. + Get the documentation for a method or method descriptor. Arguments: node: The node representing the method and its parents. @@ -803,6 +817,7 @@ def get_method_documentation(self, node: ObjectNode, properties: Optional[List[s """ method = node.obj path = node.dotted_path + signature: Optional[inspect.Signature] source: Optional[Source] try: @@ -819,12 +834,23 @@ def get_method_documentation(self, node: ObjectNode, properties: Optional[List[s else: properties.append("async") + try: + # for "built-in" functions, e.g. those implemented in C, + # inspect.signature() uses the __text_signature__ attribute, which + # provides a limited but still useful amount of signature information. + # "built-in" functions with no __text_signature__ will + # raise a ValueError(). + signature = inspect.signature(method) + except ValueError as error: + self.errors.append(f"Couldn't read signature for '{path}': {error}") + signature = None + return Method( name=node.name, path=path, file_path=node.file_path, docstring=inspect.getdoc(method), - signature=inspect.signature(method), + signature=signature, properties=properties or [], source=source, ) diff --git a/tests/fixtures/method_descriptor.py b/tests/fixtures/method_descriptor.py new file mode 100644 index 0000000..48118ab --- /dev/null +++ b/tests/fixtures/method_descriptor.py @@ -0,0 +1,3 @@ +"""See https://docs.python.org/3/library/inspect.html#inspect.ismethoddescriptor for details""" + +descriptor = int.__add__ diff --git a/tests/test_loader.py b/tests/test_loader.py index 84c1d26..b1ba0fa 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -511,3 +511,14 @@ def test_loading_cached_properties(): assert len(obj.children) == 1 assert obj.children[0].name == obj.children[0].docstring == "aaa" assert "cached" in obj.children[0].properties + + +def test_method_descriptor(): + """Load a method descriptor.""" + loader = Loader(new_path_syntax=True) + obj = loader.get_object_documentation("tests.fixtures.method_descriptor:descriptor") + assert obj.name == "descriptor" + assert obj.signature + assert len(obj.signature.parameters) == 2 + assert obj.docstring + assert obj.category == "method"