1515limitations under the License.
1616"""
1717from inspect import isclass
18- from typing import Callable , ClassVar , Dict , List , Mapping , Optional , Text , Tuple , Type , Union
18+ from typing import Callable , ClassVar , Dict , List , Mapping , Optional , Text , Tuple , Type , Union , get_type_hints , get_args
1919
2020from pydantic import BaseModel , PrivateAttr
2121import structlog # type: ignore
2222
2323from diffsync .diff import Diff
24- from diffsync .enum import DiffSyncModelFlags , DiffSyncFlags , DiffSyncStatus
25- from diffsync .exceptions import DiffClassMismatch , ObjectAlreadyExists , ObjectStoreWrongType , ObjectNotFound
24+ from diffsync .enum import DiffSyncModelFlags , DiffSyncFlags , DiffSyncStatus , DiffSyncFieldType
25+ from diffsync .exceptions import (
26+ DiffClassMismatch ,
27+ ObjectAlreadyExists ,
28+ ObjectStoreWrongType ,
29+ ObjectNotFound ,
30+ )
2631from diffsync .helpers import DiffSyncDiffer , DiffSyncSyncer
2732from diffsync .store import BaseStore
2833from diffsync .store .local import LocalStore
@@ -37,9 +42,6 @@ class DiffSyncModel(BaseModel):
3742 as model attributes and we want to avoid any ambiguity or collisions.
3843
3944 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.
4345 """
4446
4547 _modelname : ClassVar [str ] = "diffsyncmodel"
@@ -48,38 +50,13 @@ class DiffSyncModel(BaseModel):
4850 Lowercase by convention; typically corresponds to the class name, but that is not enforced.
4951 """
5052
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-
5753 _shortname : ClassVar [Tuple [str , ...]] = ()
5854 """Optional: list of model fields that together form a shorter identifier of an instance.
5955
6056 This MUST be locally unique (e.g., interface shortnames MUST be unique among all interfaces on a given device),
6157 but does not need to be guaranteed to be globally unique among all instances.
6258 """
6359
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-
8360 model_flags : DiffSyncModelFlags = DiffSyncModelFlags .NONE
8461 """Optional: any non-default behavioral flags for this DiffSyncModel.
8562
@@ -106,31 +83,32 @@ def __init_subclass__(cls):
10683
10784 Called automatically on subclass declaration.
10885 """
109- variables = cls .__fields__ .keys ()
86+ type_hints = get_type_hints (cls , include_extras = True )
87+ field_name_type_mapping = {field_type .value : [] for field_type in DiffSyncFieldType }
88+ children = {}
89+ for key , value in type_hints .items ():
90+ try :
91+ field_type = value .__metadata__ [0 ]
92+ except AttributeError :
93+ # In this case we aren't dealing with an actual payload field
94+ continue
95+
96+ if field_type in [DiffSyncFieldType .IDENTIFIER , DiffSyncFieldType .ATTRIBUTE ]:
97+ field_name_type_mapping [field_type .value ].append (key )
98+ elif field_type == DiffSyncFieldType .CHILDREN :
99+ actual_type , _ , child_modelname = get_args (value )
100+ children [child_modelname ] = key
101+
102+ cls ._identifiers = field_name_type_mapping [DiffSyncFieldType .IDENTIFIER .value ]
103+ cls ._attributes = field_name_type_mapping [DiffSyncFieldType .ATTRIBUTE .value ]
104+ cls ._children = children
105+
106+ all_field_names = cls ._identifiers + cls ._attributes + list (cls ._children .keys ())
107+
110108 # 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 } " )
114109 for attr in cls ._shortname :
115- if attr not in variables :
110+ if attr not in all_field_names :
116111 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." )
134112
135113 def __repr__ (self ):
136114 return f'{ self .get_type ()} "{ self .get_unique_id ()} "'
0 commit comments