From d61a6211aabb538920736ce88ff25b8de11fec77 Mon Sep 17 00:00:00 2001 From: Jared Khan Date: Sat, 27 Jun 2020 21:33:27 +0100 Subject: [PATCH 1/2] fix: Prevent crash in case of empty dataclasses --- src/pytkdocs/loader.py | 21 ++++++++++++--------- tests/fixtures/dataclass.py | 6 ++++++ tests/test_loader.py | 9 +++++++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/pytkdocs/loader.py b/src/pytkdocs/loader.py index 864ec27..2eda93a 100644 --- a/src/pytkdocs/loader.py +++ b/src/pytkdocs/loader.py @@ -405,15 +405,18 @@ def get_class_documentation(self, node: ObjectNode, select_members=None) -> Clas self.select_inherited_members and "__fields__" in all_members ): root_object.properties = ["dataclass"] - for field_name, annotation in all_members["__annotations__"].items(): - if self.select(field_name, select_members) and ( # type: ignore - self.select_inherited_members - # Same comment as for Pydantic models - or field_name - not in chain(*(getattr(cls, "__dataclass_fields__", {}).keys() for cls in class_.__mro__[1:-1])) - ): - child_node = ObjectNode(obj=annotation, name=field_name, parent=node) - root_object.add_child(self.get_annotated_dataclass_field(child_node)) + # In dataclasses: "A field is defined as class variable that has a type annotation" + # so we can get all fields simply by iterating through the __annotations__ + if "__annotations__" in all_members: + for field_name, annotation in all_members["__annotations__"].items(): + if self.select(field_name, select_members) and ( # type: ignore + self.select_inherited_members + # Same comment as for Pydantic models + or field_name + not in chain(*(getattr(cls, "__dataclass_fields__", {}).keys() for cls in class_.__mro__[1:-1])) + ): + child_node = ObjectNode(obj=annotation, name=field_name, parent=node) + root_object.add_child(self.get_annotated_dataclass_field(child_node)) return root_object diff --git a/tests/fixtures/dataclass.py b/tests/fixtures/dataclass.py index 199cdcd..9334b4b 100644 --- a/tests/fixtures/dataclass.py +++ b/tests/fixtures/dataclass.py @@ -8,3 +8,9 @@ class Person: name: str age: int """Field description.""" + + +@dataclass +class Empty: + """A dataclass without any fields""" + pass diff --git a/tests/test_loader.py b/tests/test_loader.py index e7e24d3..342698f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -148,6 +148,15 @@ def test_loading_dataclass(): assert "dataclass" not in not_dataclass.properties +def test_loading_empty_dataclass(): + """Handle empty dataclasses.""" + loader = Loader() + obj = loader.get_object_documentation("tests.fixtures.dataclass.Empty") + assert obj.docstring == "A dataclass without any fields" + assert len(obj.attributes) == 0 + assert "dataclass" in obj.properties + + def test_loading_pydantic_model(): """Handle Pydantic models.""" loader = Loader() From 7353559c9ac9e20cc86ef1a0e6f3a7f43330eef9 Mon Sep 17 00:00:00 2001 From: Jared Khan Date: Sun, 28 Jun 2020 10:14:50 +0100 Subject: [PATCH 2/2] fix: Use __dataclass_fields__ instead of __annotations__ --- src/pytkdocs/loader.py | 22 ++++++++++------------ tests/fixtures/dataclass.py | 1 + 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/pytkdocs/loader.py b/src/pytkdocs/loader.py index 2eda93a..36bdba0 100644 --- a/src/pytkdocs/loader.py +++ b/src/pytkdocs/loader.py @@ -405,18 +405,16 @@ def get_class_documentation(self, node: ObjectNode, select_members=None) -> Clas self.select_inherited_members and "__fields__" in all_members ): root_object.properties = ["dataclass"] - # In dataclasses: "A field is defined as class variable that has a type annotation" - # so we can get all fields simply by iterating through the __annotations__ - if "__annotations__" in all_members: - for field_name, annotation in all_members["__annotations__"].items(): - if self.select(field_name, select_members) and ( # type: ignore - self.select_inherited_members - # Same comment as for Pydantic models - or field_name - not in chain(*(getattr(cls, "__dataclass_fields__", {}).keys() for cls in class_.__mro__[1:-1])) - ): - child_node = ObjectNode(obj=annotation, name=field_name, parent=node) - root_object.add_child(self.get_annotated_dataclass_field(child_node)) + + for field in all_members["__dataclass_fields__"].values(): + if self.select(field.name, select_members) and ( # type: ignore + self.select_inherited_members + # Same comment as for Pydantic models + or field.name + not in chain(*(getattr(cls, "__dataclass_fields__", {}).keys() for cls in class_.__mro__[1:-1])) + ): + child_node = ObjectNode(obj=field.type, name=field.name, parent=node) + root_object.add_child(self.get_annotated_dataclass_field(child_node)) return root_object diff --git a/tests/fixtures/dataclass.py b/tests/fixtures/dataclass.py index 9334b4b..525a1fd 100644 --- a/tests/fixtures/dataclass.py +++ b/tests/fixtures/dataclass.py @@ -13,4 +13,5 @@ class Person: @dataclass class Empty: """A dataclass without any fields""" + pass