def test_add_children(): """Add multiple children at once.""" root = Object(name="o", path="o", file_path="o.py") class_ = Class(name="c", path="o.c", file_path="o.py") attribute = Attribute(name="a", path="o.c.a", file_path="o.py") class_.add_child(attribute) root.add_children([ # class has wrong path Class(name="w", path="wrong.path.w", file_path="/wrong/path/w.py"), # class OK class_, # not a direct child, attribute, # function OK Function(name="f", path="o.f", file_path="o.py"), # not a direct child, not even a child of known child Method(name="missing_node", path="o.mn.missing_node", file_path="o.py"), ]) assert len(root.children) == 2 assert root.classes and root.classes[0] is class_ assert root.functions and root.functions[0].name == "f"
def test_name_properties_on_class(): assert not Class(name="b", path="a.b", file_path="a.py").name_properties assert "private" in Class(name="_b", path="a._b", file_path="a.py").name_properties assert not Class(name="__b", path="a.__b", file_path="a.py").name_properties assert not Class(name="__b__", path="a.__b__", file_path="a.py").name_properties
def test_get_root(): root = Module(name="my_module", path="my.dotted.path", file_path="") node1 = Class(name="my_class1", path="my.dotted.path.my_class1", file_path="") node2 = Class(name="my_class2", path="my.dotted.path.my_class2", file_path="") leaf = Method(name="my_method", path="my.dotted.path.my_class1.my_method", file_path="") root.add_children([node1, node2]) node1.add_child(leaf) assert root.root is root assert node1.root is root assert node2.root is root assert leaf.root is root
def get_class_documentation(self, node: ObjectNode, select_members=None) -> Class: """ Get the documentation for a class and its children. Arguments: node: The node representing the class and its parents. select_members: Explicit members to select. Returns: The documented class object. """ class_ = node.obj docstring = inspect.cleandoc(class_.__doc__ or "") root_object = Class(name=node.name, path=node.dotted_path, file_path=node.file_path, docstring=docstring) # Even if we don't select members, we want to correctly parse the docstring attributes_data: Dict[str, Dict[str, Any]] = {} for parent_class in reversed(class_.__mro__[:-1]): merge(attributes_data, get_class_attributes(parent_class)) context: Dict[str, Any] = {"attributes": attributes_data} if "__init__" in class_.__dict__: try: attributes_data.update(get_instance_attributes( class_.__init__)) context["signature"] = inspect.signature(class_.__init__) except (TypeError, ValueError): pass root_object.parse_docstring(self.docstring_parser, attributes=attributes_data) if select_members is False: return root_object select_members = select_members or set() # Build the list of members members = {} inherited = set() direct_members = class_.__dict__ all_members = dict(inspect.getmembers(class_)) for member_name, member in all_members.items(): if member is class_: continue if not (member is type or member is object) and self.select( member_name, select_members): if member_name not in direct_members: if self.select_inherited_members: members[member_name] = member inherited.add(member_name) else: members[member_name] = member # Iterate on the selected members child: Object for member_name, member in members.items(): child_node = ObjectNode(member, member_name, parent=node) if child_node.is_class(): child = self.get_class_documentation(child_node) elif child_node.is_classmethod(): child = self.get_classmethod_documentation(child_node) elif child_node.is_staticmethod(): child = self.get_staticmethod_documentation(child_node) elif child_node.is_method(): child = self.get_regular_method_documentation(child_node) elif child_node.is_property(): child = self.get_property_documentation(child_node) elif member_name in attributes_data: child = self.get_attribute_documentation( child_node, attributes_data[member_name]) else: continue if member_name in inherited: child.properties.append("inherited") root_object.add_child(child) for attr_name, properties, add_method in ( ("__fields__", ["pydantic-model"], self.get_pydantic_field_documentation), ("_declared_fields", ["marshmallow-model"], self.get_marshmallow_field_documentation), ("__dataclass_fields__", ["dataclass"], self.get_annotated_dataclass_field), ): if self.detect_field_model(attr_name, direct_members, all_members): root_object.properties.extend(properties) self.add_fields( node, root_object, attr_name, all_members, select_members, class_, add_method, ) break return root_object
def get_class_documentation(self, node: ObjectNode, members=None) -> Class: """ Get the documentation for a class and its children. Arguments: node: The node representing the class and its parents. members: Explicit members to select. Return: The documented class object. """ class_ = node.obj docstring = textwrap.dedent(class_.__doc__ or "") root_object = Class(name=node.name, path=node.dotted_path, file_path=node.file_path, docstring=docstring) if members is False: return root_object members = members or set() for member_name, member in class_.__dict__.items(): if member is type or member is object: continue if not self.select(member_name, members): # type: ignore continue child_node = ObjectNode(getattr(class_, member_name), member_name, parent=node) if child_node.is_class(): root_object.add_child(self.get_class_documentation(child_node)) elif child_node.is_classmethod(): root_object.add_child( self.get_classmethod_documentation(child_node)) elif child_node.is_staticmethod(): root_object.add_child( self.get_staticmethod_documentation(child_node)) elif child_node.is_method(): root_object.add_child( self.get_regular_method_documentation(child_node)) elif child_node.is_property(): root_object.add_child( self.get_property_documentation(child_node)) # First check if this is Pydantic compatible if "__fields__" in class_.__dict__: root_object.properties = ["pydantic"] for field_name, model_field in class_.__dict__.get( "__fields__", {}).items(): if self.select(field_name, members): # type: ignore child_node = ObjectNode(obj=model_field, name=field_name, parent=node) root_object.add_child( self.get_pydantic_field_documentation(child_node)) # Handle dataclasses elif "__dataclass_fields__" in class_.__dict__: root_object.properties = ["dataclass"] for field_name, annotation in class_.__dict__.get( "__annotations__", {}).items(): if self.select(field_name, members): # type: ignore child_node = ObjectNode(obj=annotation, name=field_name, parent=node) root_object.add_child( self.get_annotated_dataclass_field(child_node)) return root_object
def get_class_documentation(self, node: ObjectNode, select_members=None) -> Class: """ Get the documentation for a class and its children. Arguments: node: The node representing the class and its parents. select_members: Explicit members to select. Return: The documented class object. """ class_ = node.obj docstring = textwrap.dedent(class_.__doc__ or "") root_object = Class(name=node.name, path=node.dotted_path, file_path=node.file_path, docstring=docstring) # Even if we don't select members, we want to correctly parse the docstring attributes_data: Dict[str, Dict[str, Any]] = {} for cls in reversed(class_.__mro__[:-1]): merge(attributes_data, get_class_attributes(cls)) context: Dict[str, Any] = {"attributes": attributes_data} if "__init__" in class_.__dict__: attributes_data.update(get_instance_attributes(class_.__init__)) context["signature"] = inspect.signature(class_.__init__) root_object.parse_docstring(self.docstring_parser, attributes=attributes_data) if select_members is False: return root_object select_members = select_members or set() # Build the list of members members = {} inherited = set() direct_members = class_.__dict__ all_members = dict(inspect.getmembers(class_)) for member_name, member in all_members.items(): if not (member is type or member is object) and self.select( member_name, select_members): if member_name not in direct_members: if self.select_inherited_members: members[member_name] = member inherited.add(member_name) else: members[member_name] = member # Iterate on the selected members child: Object for member_name, member in members.items(): child_node = ObjectNode(member, member_name, parent=node) if child_node.is_class(): child = self.get_class_documentation(child_node) elif child_node.is_classmethod(): child = self.get_classmethod_documentation(child_node) elif child_node.is_staticmethod(): child = self.get_staticmethod_documentation(child_node) elif child_node.is_method(): child = self.get_regular_method_documentation(child_node) elif child_node.is_property(): child = self.get_property_documentation(child_node) elif member_name in attributes_data: child = self.get_attribute_documentation( child_node, attributes_data[member_name]) else: continue if member_name in inherited: child.properties.append("inherited") root_object.add_child(child) # First check if this is Pydantic compatible if "__fields__" in direct_members or (self.select_inherited_members and "__fields__" in all_members): root_object.properties = ["pydantic-model"] for field_name, model_field in all_members["__fields__"].items(): if self.select( field_name, select_members ) and ( # type: ignore self.select_inherited_members # When we don't select inherited members, one way to tell if a field was inherited # is to check if it exists in parent classes __fields__ attributes. # We don't check the current class, nor the top one (object), hence __mro__[1:-1] or field_name not in chain(*(getattr(cls, "__fields__", {}).keys() for cls in class_.__mro__[1:-1]))): child_node = ObjectNode(obj=model_field, name=field_name, parent=node) root_object.add_child( self.get_pydantic_field_documentation(child_node)) # Handle dataclasses elif "__dataclass_fields__" in direct_members or ( 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)) return root_object
def test_creating_class(): assert Class(name="my_object", path="my.dotted.path", file_path="/my/absolute/path.py")