1212import warnings
1313from functools import lru_cache
1414from itertools import chain
15+ from operator import attrgetter
1516from pathlib import Path
16- from typing import Any , Dict , List , Optional , Set , Union
17+ from typing import Any , Dict , List , Mapping , Optional , Set , Tuple , Union
1718
1819from pytkdocs .objects import Attribute , Class , Function , Method , Module , Object , Source
1920from pytkdocs .parsers .attributes import get_class_attributes , get_instance_attributes , get_module_attributes , merge
@@ -508,6 +509,7 @@ def get_class_documentation(self, node: ObjectNode, select_members=None) -> Clas
508509 for attr_name , properties , add_method in (
509510 ("__fields__" , ["pydantic-model" ], self .get_pydantic_field_documentation ),
510511 ("_declared_fields" , ["marshmallow-model" ], self .get_marshmallow_field_documentation ),
512+ ("_meta.get_fields" , ["django-model" ], self .get_django_field_documentation ),
511513 ("__dataclass_fields__" , ["dataclass" ], self .get_annotated_dataclass_field ),
512514 ):
513515 if self .detect_field_model (attr_name , direct_members , all_members ):
@@ -530,14 +532,24 @@ def detect_field_model(self, attr_name: str, direct_members, all_members) -> boo
530532 Detect if an attribute is present in members.
531533
532534 Arguments:
533- attr_name: The name of the attribute to detect.
535+ attr_name: The name of the attribute to detect, can contain dots .
534536 direct_members: The direct members of the class.
535537 all_members: All members of the class.
536538
537539 Returns:
538540 Whether the attribute is present.
539541 """
540- return attr_name in direct_members or (self .select_inherited_members and attr_name in all_members )
542+
543+ first_order_attr_name , remainder = split_attr_name (attr_name )
544+ if not (
545+ first_order_attr_name in direct_members
546+ or (self .select_inherited_members and first_order_attr_name in all_members )
547+ ):
548+ return False
549+
550+ if remainder and not attrgetter (remainder )(all_members [first_order_attr_name ]):
551+ return False
552+ return True
541553
542554 def add_fields (
543555 self ,
@@ -561,7 +573,10 @@ def add_fields(
561573 base_class: The class declaring the fields.
562574 add_method: The method to add the children object.
563575 """
564- for field_name , field in members [attr_name ].items ():
576+
577+ fields = get_fields (attr_name , members = members )
578+
579+ for field_name , field in fields .items ():
565580 select_field = self .select (field_name , select_members ) # type: ignore
566581 is_inherited = field_is_inherited (field_name , attr_name , base_class )
567582
@@ -683,6 +698,35 @@ def get_pydantic_field_documentation(node: ObjectNode) -> Attribute:
683698 properties = properties ,
684699 )
685700
701+ @staticmethod
702+ def get_django_field_documentation (node : ObjectNode ) -> Attribute :
703+ """
704+ Get the documentation for a Django Field.
705+
706+ Arguments:
707+ node: The node representing the Field and its parents.
708+
709+ Returns:
710+ The documented attribute object.
711+ """
712+ prop = node .obj
713+ path = node .dotted_path
714+ properties = ["django-field" ]
715+
716+ if prop .null :
717+ properties .append ("nullable" )
718+ if prop .blank :
719+ properties .append ("blank" )
720+
721+ return Attribute (
722+ name = node .name ,
723+ path = path ,
724+ file_path = node .file_path ,
725+ docstring = prop .verbose_name ,
726+ attr_type = prop .__class__ ,
727+ properties = properties ,
728+ )
729+
686730 @staticmethod
687731 def get_marshmallow_field_documentation (node : ObjectNode ) -> Attribute :
688732 """
@@ -913,3 +957,39 @@ def field_is_inherited(field_name: str, fields_name: str, base_class: type) -> b
913957 * (getattr (parent_class , fields_name , {}).keys () for parent_class in base_class .__mro__ [1 :- 1 ]),
914958 ),
915959 )
960+
961+
962+ def split_attr_name (attr_name : str ) -> Tuple [str , Optional [str ]]:
963+ """
964+ Split an attribute name into a first-order attribute name and remainder.
965+
966+ Args:
967+ attr_name: Attribute name (a)
968+
969+ Returns:
970+ Tuple containing:
971+ first_order_attr_name: Name of the first order attribute (a)
972+ remainder: The remainder (b.c)
973+
974+ """
975+ first_order_attr_name , * remaining = attr_name .split ("." , maxsplit = 1 )
976+ remainder = remaining [0 ] if remaining else None
977+ return first_order_attr_name , remainder
978+
979+
980+ def get_fields (attr_name : str , * , members : Mapping [str , Any ] = None , class_obj = None ) -> Dict [str , Any ]:
981+ if not (bool (members ) ^ bool (class_obj )):
982+ raise ValueError ("Either members or class_obj is required." )
983+ first_order_attr_name , remainder = split_attr_name (attr_name )
984+ fields = members [first_order_attr_name ] if members else dict (vars (class_obj )).get (first_order_attr_name , {})
985+ if remainder :
986+ fields = attrgetter (remainder )(fields )
987+
988+ if callable (fields ):
989+ fields = fields ()
990+
991+ if not isinstance (fields , dict ):
992+ # Support Django models
993+ fields = {getattr (f , "name" , str (f )): f for f in fields if not getattr (f , "auto_created" , False )}
994+
995+ return fields
0 commit comments