Skip to content

Commit 52cd49b

Browse files
committed
Simplify model interface
1 parent 2967c2e commit 52cd49b

File tree

11 files changed

+133
-290
lines changed

11 files changed

+133
-290
lines changed

diffsync/__init__.py

Lines changed: 31 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@
1515
limitations under the License.
1616
"""
1717
from 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

2020
from pydantic import BaseModel, PrivateAttr
2121
import structlog # type: ignore
2222

2323
from 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+
)
2631
from diffsync.helpers import DiffSyncDiffer, DiffSyncSyncer
2732
from diffsync.store import BaseStore
2833
from 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()}"'

diffsync/enum.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,10 @@ class DiffSyncActions: # pylint: disable=too-few-public-methods
104104
DELETE = "delete"
105105
SKIP = "skip"
106106
NO_CHANGE = None
107+
108+
109+
class DiffSyncFieldType(enum.Enum):
110+
"""Enum that details which type of field a mode field is."""
111+
ATTRIBUTE = "attribute"
112+
IDENTIFIER = "identifier"
113+
CHILDREN = "children"

examples/01-multiple-data-sources/models.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,46 +14,38 @@
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
"""
17-
from typing import List, Optional
18-
from diffsync import DiffSyncModel
17+
from typing import List, Optional, Annotated
18+
from diffsync import DiffSyncModel, DiffSyncFieldType
1919

2020

2121
class Site(DiffSyncModel):
2222
"""Example model of a geographic Site."""
2323

2424
_modelname = "site"
25-
_identifiers = ("name",)
2625
_shortname = ()
27-
_attributes = ()
28-
_children = {"device": "devices"}
2926

30-
name: str
31-
devices: List = []
27+
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
28+
devices: Annotated[List, DiffSyncFieldType.CHILDREN, "device"] = []
3229

3330

3431
class Device(DiffSyncModel):
3532
"""Example model of a network Device."""
3633

3734
_modelname = "device"
38-
_identifiers = ("name",)
39-
_attributes = ()
40-
_children = {"interface": "interfaces"}
4135

42-
name: str
43-
site_name: Optional[str] # note that this attribute is NOT included in _attributes
44-
role: Optional[str] # note that this attribute is NOT included in _attributes
45-
interfaces: List = []
36+
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
37+
site_name: Optional[str] # note that this attribute is NOT annotated
38+
role: Optional[str] # note that this attribute is NOT annotated
39+
interfaces: Annotated[List, DiffSyncFieldType.CHILDREN, "interface"] = []
4640

4741

4842
class Interface(DiffSyncModel):
4943
"""Example model of a network Interface."""
5044

5145
_modelname = "interface"
52-
_identifiers = ("device_name", "name")
5346
_shortname = ("name",)
54-
_attributes = ("description",)
5547

56-
name: str
57-
device_name: str
48+
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
49+
device_name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
5850

59-
description: Optional[str]
51+
description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]

examples/02-callback-function/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@
1616
limitations under the License.
1717
"""
1818
import random
19+
from typing import Annotated
1920

20-
from diffsync import DiffSync, DiffSyncModel
21+
from diffsync import DiffSync, DiffSyncModel, DiffSyncFieldType
2122
from diffsync.logging import enable_console_logging
2223

2324

2425
class Number(DiffSyncModel):
2526
"""Simple model that consists only of a number."""
2627

2728
_modelname = "number"
28-
_identifiers = ("number",)
2929

30-
number: int
30+
number: Annotated[int, DiffSyncFieldType.IDENTIFIER]
3131

3232

3333
class DiffSync1(DiffSync):
Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
"""Main DiffSync models for example3."""
2-
from typing import List, Optional
3-
from diffsync import DiffSyncModel
2+
from typing import List, Optional, Annotated
3+
from diffsync import DiffSyncModel, DiffSyncFieldType
44

55

66
class Region(DiffSyncModel):
77
"""Example model of a geographic region."""
88

99
_modelname = "region"
10-
_identifiers = ("slug",)
11-
_attributes = ("name",)
1210

13-
# By listing country as a child to Region
14-
# DiffSync will be able to recursively compare all regions including all their children
15-
_children = {"country": "countries"}
11+
slug: Annotated[str, DiffSyncFieldType.IDENTIFIER]
12+
name: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
1613

17-
slug: str
18-
name: str
19-
countries: List[str] = []
14+
# By annotating country as a child to Region
15+
# DiffSync will be able to recursively compare all regions including all their children
16+
countries: Annotated[List[str], DiffSyncFieldType.CHILDREN, "country"] = []
2017

2118

2219
class Country(DiffSyncModel):
@@ -26,10 +23,8 @@ class Country(DiffSyncModel):
2623
"""
2724

2825
_modelname = "country"
29-
_identifiers = ("slug",)
30-
_attributes = ("name", "region", "population")
3126

32-
slug: str
33-
name: str
34-
region: str
35-
population: Optional[int]
27+
slug: Annotated[str, DiffSyncFieldType.IDENTIFIER]
28+
name: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
29+
region: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
30+
population: Annotated[Optional[int], DiffSyncFieldType.ATTRIBUTE]

examples/04-get-update-instantiate/models.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,45 +14,38 @@
1414
See the License for the specific language governing permissions and
1515
limitations under the License.
1616
"""
17-
from typing import List, Optional
18-
from diffsync import DiffSyncModel
17+
from typing import List, Optional, Annotated
18+
from diffsync import DiffSyncModel, DiffSyncFieldType
1919

2020

2121
class Site(DiffSyncModel):
2222
"""Example model of a geographic Site."""
2323

2424
_modelname = "site"
25-
_identifiers = ("name",)
2625
_shortname = ()
27-
_attributes = ()
2826

29-
name: str
27+
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
3028

3129

3230
class Device(DiffSyncModel):
3331
"""Example model of a network Device."""
3432

3533
_modelname = "device"
36-
_identifiers = ("name",)
37-
_attributes = ()
38-
_children = {"interface": "interfaces", "site": "sites"}
3934

40-
name: str
41-
site_name: Optional[str] # note that this attribute is NOT included in _attributes
42-
role: Optional[str] # note that this attribute is NOT included in _attributes
43-
interfaces: List = []
44-
sites: List = []
35+
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
36+
site_name: Optional[str] # note that this attribute is NOT annotated
37+
role: Optional[str] # note that this attribute is NOT annotated
38+
interfaces: Annotated[List[str], DiffSyncFieldType.CHILDREN, "interface"] = []
39+
sites: Annotated[List[str], DiffSyncFieldType.CHILDREN, "site"] = []
4540

4641

4742
class Interface(DiffSyncModel):
4843
"""Example model of a network Interface."""
4944

5045
_modelname = "interface"
51-
_identifiers = ("device_name", "name")
5246
_shortname = ("name",)
53-
_attributes = ("description",)
5447

55-
name: str
56-
device_name: str
48+
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
49+
device_name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
5750

58-
description: Optional[str]
51+
description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,24 @@
11
"""DiffSyncModel subclasses for Nautobot-PeeringDB data sync."""
2-
from typing import Optional, Union, List
2+
from typing import Optional, Union, List, Annotated
33
from uuid import UUID
44

5-
from diffsync import DiffSyncModel
5+
from diffsync import DiffSyncModel, DiffSyncFieldType
66

77

88
class RegionModel(DiffSyncModel):
99
"""Shared data model representing a Region."""
1010

1111
# Metadata about this model
1212
_modelname = "region"
13-
_identifiers = ("name",)
14-
_attributes = (
15-
"slug",
16-
"description",
17-
"parent_name",
18-
)
19-
_children = {"site": "sites"}
2013

2114
# Data type declarations for all identifiers and attributes
22-
name: str
23-
slug: str
24-
description: Optional[str]
25-
parent_name: Optional[str] # may be None
26-
sites: List = []
15+
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
16+
slug: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
17+
description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
18+
parent_name: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
19+
sites: Annotated[List, DiffSyncFieldType.CHILDREN, "sites"] = []
2720

28-
# Not in _attributes or _identifiers, hence not included in diff calculations
21+
# Not annotated, hence not included in diff calculations
2922
pk: Optional[UUID]
3023

3124

@@ -34,25 +27,15 @@ class SiteModel(DiffSyncModel):
3427

3528
# Metadata about this model
3629
_modelname = "site"
37-
_identifiers = ("name",)
38-
# To keep this example simple, we don't include **all** attributes of a Site here. But you could!
39-
_attributes = (
40-
"slug",
41-
"status_slug",
42-
"region_name",
43-
"description",
44-
"latitude",
45-
"longitude",
46-
)
4730

48-
# Data type declarations for all identifiers and attributes
49-
name: str
50-
slug: str
51-
status_slug: str
52-
region_name: Optional[str] # may be None
53-
description: Optional[str]
54-
latitude: Optional[float]
55-
longitude: Optional[float]
31+
# To keep this example simple, we don't include **all** attributes of a Site here. But you could!
32+
name: Annotated[str, DiffSyncFieldType.IDENTIFIER]
33+
slug: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
34+
status_slug: Annotated[str, DiffSyncFieldType.ATTRIBUTE]
35+
region_name: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
36+
description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE]
37+
latitude: Annotated[Optional[float], DiffSyncFieldType.ATTRIBUTE]
38+
longitude: Annotated[Optional[float], DiffSyncFieldType.ATTRIBUTE]
5639

5740
# Not in _attributes or _identifiers, hence not included in diff calculations
5841
pk: Optional[Union[UUID, int]]

0 commit comments

Comments
 (0)