12
12
import warnings
13
13
from functools import lru_cache
14
14
from itertools import chain
15
+ from operator import attrgetter
15
16
from 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
17
18
18
19
from pytkdocs .objects import Attribute , Class , Function , Method , Module , Object , Source
19
20
from 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
508
509
for attr_name , properties , add_method in (
509
510
("__fields__" , ["pydantic-model" ], self .get_pydantic_field_documentation ),
510
511
("_declared_fields" , ["marshmallow-model" ], self .get_marshmallow_field_documentation ),
512
+ ("_meta.get_fields" , ["django-model" ], self .get_django_field_documentation ),
511
513
("__dataclass_fields__" , ["dataclass" ], self .get_annotated_dataclass_field ),
512
514
):
513
515
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
530
532
Detect if an attribute is present in members.
531
533
532
534
Arguments:
533
- attr_name: The name of the attribute to detect.
535
+ attr_name: The name of the attribute to detect, can contain dots .
534
536
direct_members: The direct members of the class.
535
537
all_members: All members of the class.
536
538
537
539
Returns:
538
540
Whether the attribute is present.
539
541
"""
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
541
553
542
554
def add_fields (
543
555
self ,
@@ -561,7 +573,10 @@ def add_fields(
561
573
base_class: The class declaring the fields.
562
574
add_method: The method to add the children object.
563
575
"""
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 ():
565
580
select_field = self .select (field_name , select_members ) # type: ignore
566
581
is_inherited = field_is_inherited (field_name , attr_name , base_class )
567
582
@@ -683,6 +698,35 @@ def get_pydantic_field_documentation(node: ObjectNode) -> Attribute:
683
698
properties = properties ,
684
699
)
685
700
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
+
686
730
@staticmethod
687
731
def get_marshmallow_field_documentation (node : ObjectNode ) -> Attribute :
688
732
"""
@@ -913,3 +957,39 @@ def field_is_inherited(field_name: str, fields_name: str, base_class: type) -> b
913
957
* (getattr (parent_class , fields_name , {}).keys () for parent_class in base_class .__mro__ [1 :- 1 ]),
914
958
),
915
959
)
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