1414See the License for the specific language governing permissions and
1515limitations under the License.
1616"""
17+ from functools import cached_property
1718from inspect import isclass
18- from typing import Callable , ClassVar , Dict , List , Mapping , Optional , Text , Tuple , Type , Union
19+ from typing import Callable , ClassVar , Dict , List , Mapping , Optional , Text , Tuple , Type , Union , get_type_hints , get_args
1920
2021from pydantic import BaseModel , PrivateAttr
2122import structlog # type: ignore
2223
2324from diffsync .diff import Diff
24- from diffsync .enum import DiffSyncModelFlags , DiffSyncFlags , DiffSyncStatus
25- from diffsync .exceptions import DiffClassMismatch , ObjectAlreadyExists , ObjectStoreWrongType , ObjectNotFound
25+ from diffsync .enum import DiffSyncModelFlags , DiffSyncFlags , DiffSyncStatus , DiffSyncFieldType
26+ from diffsync .exceptions import (
27+ DiffClassMismatch ,
28+ ObjectAlreadyExists ,
29+ ObjectStoreWrongType ,
30+ ObjectNotFound ,
31+ ModelChildrenException ,
32+ )
2633from diffsync .helpers import DiffSyncDiffer , DiffSyncSyncer
2734from diffsync .store import BaseStore
2835from diffsync .store .local import LocalStore
@@ -37,9 +44,6 @@ class DiffSyncModel(BaseModel):
3744 as model attributes and we want to avoid any ambiguity or collisions.
3845
3946 This class has several underscore-prefixed class variables that subclasses should set as desired; see below.
40-
41- NOTE: The groupings _identifiers, _attributes, and _children are mutually exclusive; any given field name can
42- be included in **at most** one of these three tuples.
4347 """
4448
4549 _modelname : ClassVar [str ] = "diffsyncmodel"
@@ -48,38 +52,13 @@ class DiffSyncModel(BaseModel):
4852 Lowercase by convention; typically corresponds to the class name, but that is not enforced.
4953 """
5054
51- _identifiers : ClassVar [Tuple [str , ...]] = ()
52- """List of model fields which together uniquely identify an instance of this model.
53-
54- This identifier MUST be globally unique among all instances of this class.
55- """
56-
5755 _shortname : ClassVar [Tuple [str , ...]] = ()
5856 """Optional: list of model fields that together form a shorter identifier of an instance.
5957
6058 This MUST be locally unique (e.g., interface shortnames MUST be unique among all interfaces on a given device),
6159 but does not need to be guaranteed to be globally unique among all instances.
6260 """
6361
64- _attributes : ClassVar [Tuple [str , ...]] = ()
65- """Optional: list of additional model fields (beyond those in `_identifiers`) that are relevant to this model.
66-
67- Only the fields in `_attributes` (as well as any `_children` fields, see below) will be considered
68- for the purposes of Diff calculation.
69- A model may define additional fields (not included in `_attributes`) for its internal use;
70- a common example would be a locally significant database primary key or id value.
71-
72- Note: inclusion in `_attributes` is mutually exclusive from inclusion in `_identifiers`; a field cannot be in both!
73- """
74-
75- _children : ClassVar [Mapping [str , str ]] = {}
76- """Optional: dict of `{_modelname: field_name}` entries describing how to store "child" models in this model.
77-
78- When calculating a Diff or performing a sync, DiffSync will automatically recurse into these child models.
79-
80- Note: inclusion in `_children` is mutually exclusive from inclusion in `_identifiers` or `_attributes`.
81- """
82-
8362 model_flags : DiffSyncModelFlags = DiffSyncModelFlags .NONE
8463 """Optional: any non-default behavioral flags for this DiffSyncModel.
8564
@@ -106,31 +85,32 @@ def __init_subclass__(cls):
10685
10786 Called automatically on subclass declaration.
10887 """
109- variables = cls .__fields__ .keys ()
88+ type_hints = get_type_hints (cls , include_extras = True )
89+ field_name_type_mapping = {field_type .value : [] for field_type in DiffSyncFieldType }
90+ children = {}
91+ for key , value in type_hints .items ():
92+ try :
93+ field_type = value .__metadata__ [0 ]
94+ except AttributeError :
95+ # In this case we aren't dealing with an actual payload field
96+ continue
97+
98+ if field_type in [DiffSyncFieldType .IDENTIFIER , DiffSyncFieldType .ATTRIBUTE ]:
99+ field_name_type_mapping [field_type .value ].append (key )
100+ elif field_type == DiffSyncFieldType .CHILDREN :
101+ actual_type , _ , child_modelname = get_args (value )
102+ children [child_modelname ] = key
103+
104+ cls ._identifiers = field_name_type_mapping [DiffSyncFieldType .IDENTIFIER .value ]
105+ cls ._attributes = field_name_type_mapping [DiffSyncFieldType .ATTRIBUTE .value ]
106+ cls ._children = children
107+
108+ all_field_names = cls ._identifiers + cls ._attributes + list (cls ._children .keys ())
109+
110110 # Make sure that any field referenced by name actually exists on the model
111- for attr in cls ._identifiers :
112- if attr not in variables and not hasattr (cls , attr ):
113- raise AttributeError (f"_identifiers { cls ._identifiers } references missing or un-annotated attr { attr } " )
114111 for attr in cls ._shortname :
115- if attr not in variables :
112+ if attr not in all_field_names :
116113 raise AttributeError (f"_shortname { cls ._shortname } references missing or un-annotated attr { attr } " )
117- for attr in cls ._attributes :
118- if attr not in variables :
119- raise AttributeError (f"_attributes { cls ._attributes } references missing or un-annotated attr { attr } " )
120- for attr in cls ._children .values ():
121- if attr not in variables :
122- raise AttributeError (f"_children { cls ._children } references missing or un-annotated attr { attr } " )
123-
124- # Any given field can only be in one of (_identifiers, _attributes, _children)
125- id_attr_overlap = set (cls ._identifiers ).intersection (cls ._attributes )
126- if id_attr_overlap :
127- raise AttributeError (f"Fields { id_attr_overlap } are included in both _identifiers and _attributes." )
128- id_child_overlap = set (cls ._identifiers ).intersection (cls ._children .values ())
129- if id_child_overlap :
130- raise AttributeError (f"Fields { id_child_overlap } are included in both _identifiers and _children." )
131- attr_child_overlap = set (cls ._attributes ).intersection (cls ._children .values ())
132- if attr_child_overlap :
133- raise AttributeError (f"Fields { attr_child_overlap } are included in both _attributes and _children." )
134114
135115 def __repr__ (self ):
136116 return f'{ self .get_type ()} "{ self .get_unique_id ()} "'
0 commit comments