1- from typing import Any , Iterator , Tuple
1+ """Composite fields for handling collections of resources."""
2+ import abc
3+ from functools import cached_property
4+ from typing import Any , Callable , Iterator , Tuple
25
36from odin import bases , exceptions
47from odin .fields import Field
1619)
1720
1821
19- class CompositeField (Field ):
20- """
21- The base class for composite (or fields that contain other resources) eg DictAs/ListOf fields.
22- """
22+ class CompositeField (Field , metaclass = abc .ABCMeta ):
23+ """The base class for composite.
24+
25+ Fields that contain other resources eg DictAs/ListOf fields."""
26+
27+ @classmethod
28+ def delayed (cls , resource_callable : Callable [[], Any ], ** options ):
29+ """Create a delayed resource field.
30+
31+ This is used in the case of tree structures where a resource may reference itself.
32+
33+ This should be used with a lambda function to avoid referencing an incomplete type.
34+
35+ .. code-block:: python
36+
37+ class Category(odin.Resource):
38+ name = odin.StringField()
39+ child_categories = odin.DictAs.delayed(lambda: Category)
2340
24- def __init__ (self , resource , use_container = False , ** options ):
2541 """
26- Initialisation of a CompositeField.
42+ return cls (resource_callable , ** options )
43+
44+ def __init__ (self , resource , use_container = False , ** options ):
45+ """Initialisation of a CompositeField.
2746
2847 :param resource:
2948 :param use_container: Special flag for codecs that support containers or just multiple instances of a
@@ -32,16 +51,37 @@ def __init__(self, resource, use_container=False, **options):
3251 :param options: Additional options passed to :py:class:`odin.fields.Field` super class.
3352
3453 """
54+
3555 if not hasattr (resource , "_meta" ):
36- raise TypeError (f"{ resource !r} is not a valid type for a related field." )
37- self .of = resource
56+ if callable (resource ):
57+ # Delayed resolution of the resource type.
58+ self ._of = resource
59+ else :
60+ # Keep this pattern so old behaviour remains.
61+ raise TypeError (
62+ f"{ resource !r} is not a valid type for a related field."
63+ )
64+ else :
65+ self ._of = resource
3866 self .use_container = use_container
3967
4068 if not options .get ("null" , False ):
41- options .setdefault ("default" , lambda : resource ())
69+ options .setdefault ("default" , lambda : self . of ())
4270
4371 super ().__init__ (** options )
4472
73+ @cached_property
74+ def of (self ):
75+ """Return the resource type."""
76+ resource = self ._of
77+ if not hasattr (resource , "_meta" ) and callable (resource ):
78+ resource = resource ()
79+ if not hasattr (resource , "_meta" ):
80+ raise TypeError (
81+ f"{ resource !r} is not a valid type for a related field."
82+ )
83+ return resource
84+
4585 def to_python (self , value ):
4686 """Convert raw value to a python value."""
4787 if value is None :
@@ -59,24 +99,16 @@ def validate(self, value):
5999 if value not in EMPTY_VALUES :
60100 value .full_clean ()
61101
102+ @abc .abstractmethod
62103 def item_iter_from_object (self , obj ):
63- """
64- Return an iterator of items (resource, idx) from composite field.
104+ """Return an iterator of items (resource, idx) from composite field.
65105
66106 For single items (eg ``DictAs`` will return a list a single item (resource, None))
67-
68- :param obj:
69- :return:
70107 """
71- raise NotImplementedError ()
72108
109+ @abc .abstractmethod
73110 def key_to_python (self , key ):
74- """
75- A to python method for the key value.
76- :param key:
77- :return:
78- """
79- raise NotImplementedError ()
111+ """A to python method for the key value."""
80112
81113
82114class DictAs (CompositeField ):
@@ -243,7 +275,7 @@ def validate(self, value):
243275 raise exceptions .ValidationError (self .error_messages ["empty" ], code = "empty" )
244276
245277 def __iter__ (self ):
246- # This does nothing but it does prevent inspections from complaining.
278+ # This does nothing, it does prevent inspections from complaining.
247279 return None # NoQA
248280
249281 def item_iter_from_object (self , obj ) -> Iterator [Tuple [str , Any ]]:
0 commit comments