From 723f88f5ee561cf1770fdcd86a1dc65864315066 Mon Sep 17 00:00:00 2001 From: Leo Kirchner Date: Tue, 20 Jun 2023 16:53:24 +0200 Subject: [PATCH] Simplify model interface --- diffsync/__init__.py | 84 ++++++--------- diffsync/enum.py | 7 ++ examples/01-multiple-data-sources/models.py | 30 ++---- examples/02-callback-function/main.py | 6 +- examples/03-remote-system/models.py | 27 ++--- examples/04-get-update-instantiate/models.py | 29 ++---- examples/05-nautobot-peeringdb/models.py | 49 +++------ examples/06-ip-prefixes/models.py | 14 ++- tests/unit/conftest.py | 63 +++++------- tests/unit/test_diffsync_model.py | 101 ++----------------- tests/unit/test_diffsync_model_flags.py | 13 +-- 11 files changed, 133 insertions(+), 290 deletions(-) diff --git a/diffsync/__init__.py b/diffsync/__init__.py index 3fbe7186..cd282c2e 100644 --- a/diffsync/__init__.py +++ b/diffsync/__init__.py @@ -15,14 +15,19 @@ limitations under the License. """ from inspect import isclass -from typing import Callable, ClassVar, Dict, List, Mapping, Optional, Text, Tuple, Type, Union +from typing import Callable, ClassVar, Dict, List, Mapping, Optional, Text, Tuple, Type, Union, get_type_hints, get_args from pydantic import BaseModel, PrivateAttr import structlog # type: ignore from diffsync.diff import Diff -from diffsync.enum import DiffSyncModelFlags, DiffSyncFlags, DiffSyncStatus -from diffsync.exceptions import DiffClassMismatch, ObjectAlreadyExists, ObjectStoreWrongType, ObjectNotFound +from diffsync.enum import DiffSyncModelFlags, DiffSyncFlags, DiffSyncStatus, DiffSyncFieldType +from diffsync.exceptions import ( + DiffClassMismatch, + ObjectAlreadyExists, + ObjectStoreWrongType, + ObjectNotFound, +) from diffsync.helpers import DiffSyncDiffer, DiffSyncSyncer from diffsync.store import BaseStore from diffsync.store.local import LocalStore @@ -37,9 +42,6 @@ class DiffSyncModel(BaseModel): as model attributes and we want to avoid any ambiguity or collisions. This class has several underscore-prefixed class variables that subclasses should set as desired; see below. - - NOTE: The groupings _identifiers, _attributes, and _children are mutually exclusive; any given field name can - be included in **at most** one of these three tuples. """ _modelname: ClassVar[str] = "diffsyncmodel" @@ -48,12 +50,6 @@ class DiffSyncModel(BaseModel): Lowercase by convention; typically corresponds to the class name, but that is not enforced. """ - _identifiers: ClassVar[Tuple[str, ...]] = () - """List of model fields which together uniquely identify an instance of this model. - - This identifier MUST be globally unique among all instances of this class. - """ - _shortname: ClassVar[Tuple[str, ...]] = () """Optional: list of model fields that together form a shorter identifier of an instance. @@ -61,25 +57,6 @@ class DiffSyncModel(BaseModel): but does not need to be guaranteed to be globally unique among all instances. """ - _attributes: ClassVar[Tuple[str, ...]] = () - """Optional: list of additional model fields (beyond those in `_identifiers`) that are relevant to this model. - - Only the fields in `_attributes` (as well as any `_children` fields, see below) will be considered - for the purposes of Diff calculation. - A model may define additional fields (not included in `_attributes`) for its internal use; - a common example would be a locally significant database primary key or id value. - - Note: inclusion in `_attributes` is mutually exclusive from inclusion in `_identifiers`; a field cannot be in both! - """ - - _children: ClassVar[Mapping[str, str]] = {} - """Optional: dict of `{_modelname: field_name}` entries describing how to store "child" models in this model. - - When calculating a Diff or performing a sync, DiffSync will automatically recurse into these child models. - - Note: inclusion in `_children` is mutually exclusive from inclusion in `_identifiers` or `_attributes`. - """ - model_flags: DiffSyncModelFlags = DiffSyncModelFlags.NONE """Optional: any non-default behavioral flags for this DiffSyncModel. @@ -106,31 +83,32 @@ def __init_subclass__(cls): Called automatically on subclass declaration. """ - variables = cls.__fields__.keys() + type_hints = get_type_hints(cls, include_extras=True) + field_name_type_mapping = {field_type.value: [] for field_type in DiffSyncFieldType} + children = {} + for key, value in type_hints.items(): + try: + field_type = value.__metadata__[0] + except AttributeError: + # In this case we aren't dealing with an actual payload field + continue + + if field_type in [DiffSyncFieldType.IDENTIFIER, DiffSyncFieldType.ATTRIBUTE]: + field_name_type_mapping[field_type.value].append(key) + elif field_type == DiffSyncFieldType.CHILDREN: + actual_type, _, child_modelname = get_args(value) + children[child_modelname] = key + + cls._identifiers = field_name_type_mapping[DiffSyncFieldType.IDENTIFIER.value] + cls._attributes = field_name_type_mapping[DiffSyncFieldType.ATTRIBUTE.value] + cls._children = children + + all_field_names = cls._identifiers + cls._attributes + list(cls._children.keys()) + # Make sure that any field referenced by name actually exists on the model - for attr in cls._identifiers: - if attr not in variables and not hasattr(cls, attr): - raise AttributeError(f"_identifiers {cls._identifiers} references missing or un-annotated attr {attr}") for attr in cls._shortname: - if attr not in variables: + if attr not in all_field_names: raise AttributeError(f"_shortname {cls._shortname} references missing or un-annotated attr {attr}") - for attr in cls._attributes: - if attr not in variables: - raise AttributeError(f"_attributes {cls._attributes} references missing or un-annotated attr {attr}") - for attr in cls._children.values(): - if attr not in variables: - raise AttributeError(f"_children {cls._children} references missing or un-annotated attr {attr}") - - # Any given field can only be in one of (_identifiers, _attributes, _children) - id_attr_overlap = set(cls._identifiers).intersection(cls._attributes) - if id_attr_overlap: - raise AttributeError(f"Fields {id_attr_overlap} are included in both _identifiers and _attributes.") - id_child_overlap = set(cls._identifiers).intersection(cls._children.values()) - if id_child_overlap: - raise AttributeError(f"Fields {id_child_overlap} are included in both _identifiers and _children.") - attr_child_overlap = set(cls._attributes).intersection(cls._children.values()) - if attr_child_overlap: - raise AttributeError(f"Fields {attr_child_overlap} are included in both _attributes and _children.") def __repr__(self): return f'{self.get_type()} "{self.get_unique_id()}"' diff --git a/diffsync/enum.py b/diffsync/enum.py index cb56532f..6cbf657f 100644 --- a/diffsync/enum.py +++ b/diffsync/enum.py @@ -104,3 +104,10 @@ class DiffSyncActions: # pylint: disable=too-few-public-methods DELETE = "delete" SKIP = "skip" NO_CHANGE = None + + +class DiffSyncFieldType(enum.Enum): + """Enum that details which type of field a mode field is.""" + ATTRIBUTE = "attribute" + IDENTIFIER = "identifier" + CHILDREN = "children" diff --git a/examples/01-multiple-data-sources/models.py b/examples/01-multiple-data-sources/models.py index 29085155..2c756e94 100644 --- a/examples/01-multiple-data-sources/models.py +++ b/examples/01-multiple-data-sources/models.py @@ -14,46 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. """ -from typing import List, Optional -from diffsync import DiffSyncModel +from typing import List, Optional, Annotated +from diffsync import DiffSyncModel, DiffSyncFieldType class Site(DiffSyncModel): """Example model of a geographic Site.""" _modelname = "site" - _identifiers = ("name",) _shortname = () - _attributes = () - _children = {"device": "devices"} - name: str - devices: List = [] + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + devices: Annotated[List, DiffSyncFieldType.CHILDREN, "device"] = [] class Device(DiffSyncModel): """Example model of a network Device.""" _modelname = "device" - _identifiers = ("name",) - _attributes = () - _children = {"interface": "interfaces"} - name: str - site_name: Optional[str] # note that this attribute is NOT included in _attributes - role: Optional[str] # note that this attribute is NOT included in _attributes - interfaces: List = [] + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + site_name: Optional[str] # note that this attribute is NOT annotated + role: Optional[str] # note that this attribute is NOT annotated + interfaces: Annotated[List, DiffSyncFieldType.CHILDREN, "interface"] = [] class Interface(DiffSyncModel): """Example model of a network Interface.""" _modelname = "interface" - _identifiers = ("device_name", "name") _shortname = ("name",) - _attributes = ("description",) - name: str - device_name: str + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + device_name: Annotated[str, DiffSyncFieldType.IDENTIFIER] - description: Optional[str] + description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE] diff --git a/examples/02-callback-function/main.py b/examples/02-callback-function/main.py index 514f2cb7..3c1f9c5b 100755 --- a/examples/02-callback-function/main.py +++ b/examples/02-callback-function/main.py @@ -16,8 +16,9 @@ limitations under the License. """ import random +from typing import Annotated -from diffsync import DiffSync, DiffSyncModel +from diffsync import DiffSync, DiffSyncModel, DiffSyncFieldType from diffsync.logging import enable_console_logging @@ -25,9 +26,8 @@ class Number(DiffSyncModel): """Simple model that consists only of a number.""" _modelname = "number" - _identifiers = ("number",) - number: int + number: Annotated[int, DiffSyncFieldType.IDENTIFIER] class DiffSync1(DiffSync): diff --git a/examples/03-remote-system/models.py b/examples/03-remote-system/models.py index 6a1f85c1..d6e51e03 100644 --- a/examples/03-remote-system/models.py +++ b/examples/03-remote-system/models.py @@ -1,22 +1,19 @@ """Main DiffSync models for example3.""" -from typing import List, Optional -from diffsync import DiffSyncModel +from typing import List, Optional, Annotated +from diffsync import DiffSyncModel, DiffSyncFieldType class Region(DiffSyncModel): """Example model of a geographic region.""" _modelname = "region" - _identifiers = ("slug",) - _attributes = ("name",) - # By listing country as a child to Region - # DiffSync will be able to recursively compare all regions including all their children - _children = {"country": "countries"} + slug: Annotated[str, DiffSyncFieldType.IDENTIFIER] + name: Annotated[str, DiffSyncFieldType.ATTRIBUTE] - slug: str - name: str - countries: List[str] = [] + # By annotating country as a child to Region + # DiffSync will be able to recursively compare all regions including all their children + countries: Annotated[List[str], DiffSyncFieldType.CHILDREN, "country"] = [] class Country(DiffSyncModel): @@ -26,10 +23,8 @@ class Country(DiffSyncModel): """ _modelname = "country" - _identifiers = ("slug",) - _attributes = ("name", "region", "population") - slug: str - name: str - region: str - population: Optional[int] + slug: Annotated[str, DiffSyncFieldType.IDENTIFIER] + name: Annotated[str, DiffSyncFieldType.ATTRIBUTE] + region: Annotated[str, DiffSyncFieldType.ATTRIBUTE] + population: Annotated[Optional[int], DiffSyncFieldType.ATTRIBUTE] diff --git a/examples/04-get-update-instantiate/models.py b/examples/04-get-update-instantiate/models.py index 9a105992..f019c232 100644 --- a/examples/04-get-update-instantiate/models.py +++ b/examples/04-get-update-instantiate/models.py @@ -14,45 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. """ -from typing import List, Optional -from diffsync import DiffSyncModel +from typing import List, Optional, Annotated +from diffsync import DiffSyncModel, DiffSyncFieldType class Site(DiffSyncModel): """Example model of a geographic Site.""" _modelname = "site" - _identifiers = ("name",) _shortname = () - _attributes = () - name: str + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] class Device(DiffSyncModel): """Example model of a network Device.""" _modelname = "device" - _identifiers = ("name",) - _attributes = () - _children = {"interface": "interfaces", "site": "sites"} - name: str - site_name: Optional[str] # note that this attribute is NOT included in _attributes - role: Optional[str] # note that this attribute is NOT included in _attributes - interfaces: List = [] - sites: List = [] + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + site_name: Optional[str] # note that this attribute is NOT annotated + role: Optional[str] # note that this attribute is NOT annotated + interfaces: Annotated[List[str], DiffSyncFieldType.CHILDREN, "interface"] = [] + sites: Annotated[List[str], DiffSyncFieldType.CHILDREN, "site"] = [] class Interface(DiffSyncModel): """Example model of a network Interface.""" _modelname = "interface" - _identifiers = ("device_name", "name") _shortname = ("name",) - _attributes = ("description",) - name: str - device_name: str + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + device_name: Annotated[str, DiffSyncFieldType.IDENTIFIER] - description: Optional[str] + description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE] diff --git a/examples/05-nautobot-peeringdb/models.py b/examples/05-nautobot-peeringdb/models.py index 2fecb044..5204a9a2 100644 --- a/examples/05-nautobot-peeringdb/models.py +++ b/examples/05-nautobot-peeringdb/models.py @@ -1,8 +1,8 @@ """DiffSyncModel subclasses for Nautobot-PeeringDB data sync.""" -from typing import Optional, Union, List +from typing import Optional, Union, List, Annotated from uuid import UUID -from diffsync import DiffSyncModel +from diffsync import DiffSyncModel, DiffSyncFieldType class RegionModel(DiffSyncModel): @@ -10,22 +10,15 @@ class RegionModel(DiffSyncModel): # Metadata about this model _modelname = "region" - _identifiers = ("name",) - _attributes = ( - "slug", - "description", - "parent_name", - ) - _children = {"site": "sites"} # Data type declarations for all identifiers and attributes - name: str - slug: str - description: Optional[str] - parent_name: Optional[str] # may be None - sites: List = [] + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + slug: Annotated[str, DiffSyncFieldType.ATTRIBUTE] + description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE] + parent_name: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE] + sites: Annotated[List, DiffSyncFieldType.CHILDREN, "sites"] = [] - # Not in _attributes or _identifiers, hence not included in diff calculations + # Not annotated, hence not included in diff calculations pk: Optional[UUID] @@ -34,25 +27,15 @@ class SiteModel(DiffSyncModel): # Metadata about this model _modelname = "site" - _identifiers = ("name",) - # To keep this example simple, we don't include **all** attributes of a Site here. But you could! - _attributes = ( - "slug", - "status_slug", - "region_name", - "description", - "latitude", - "longitude", - ) - # Data type declarations for all identifiers and attributes - name: str - slug: str - status_slug: str - region_name: Optional[str] # may be None - description: Optional[str] - latitude: Optional[float] - longitude: Optional[float] + # To keep this example simple, we don't include **all** attributes of a Site here. But you could! + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + slug: Annotated[str, DiffSyncFieldType.ATTRIBUTE] + status_slug: Annotated[str, DiffSyncFieldType.ATTRIBUTE] + region_name: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE] + description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE] + latitude: Annotated[Optional[float], DiffSyncFieldType.ATTRIBUTE] + longitude: Annotated[Optional[float], DiffSyncFieldType.ATTRIBUTE] # Not in _attributes or _identifiers, hence not included in diff calculations pk: Optional[Union[UUID, int]] diff --git a/examples/06-ip-prefixes/models.py b/examples/06-ip-prefixes/models.py index 2fc4abdd..08b79caf 100644 --- a/examples/06-ip-prefixes/models.py +++ b/examples/06-ip-prefixes/models.py @@ -1,16 +1,14 @@ """DiffSync models.""" -from typing import Optional -from diffsync import DiffSyncModel +from typing import Optional, Annotated +from diffsync import DiffSyncModel, DiffSyncFieldType class Prefix(DiffSyncModel): """Example model of a Prefix.""" _modelname = "prefix" - _identifiers = ("prefix",) - _attributes = ("vrf", "vlan_id", "tenant") - prefix: str - vrf: Optional[str] - vlan_id: Optional[int] - tenant: Optional[str] + prefix: Annotated[str, DiffSyncFieldType.IDENTIFIER] + vrf: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE] + vlan_id: Annotated[Optional[int], DiffSyncFieldType.ATTRIBUTE] + tenant: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE] diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 07b8fc8d..6b102b27 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. """ -from typing import ClassVar, List, Mapping, Optional, Tuple +from typing import ClassVar, List, Mapping, Optional, Annotated import pytest -from diffsync import DiffSync, DiffSyncModel +from diffsync import DiffSync, DiffSyncModel, DiffSyncFieldType from diffsync.diff import Diff, DiffElement from diffsync.exceptions import ObjectNotCreated, ObjectNotUpdated, ObjectNotDeleted @@ -26,7 +26,12 @@ @pytest.fixture def generic_diffsync_model(): """Provide a generic DiffSyncModel instance.""" - return DiffSyncModel() + + # Create a subclass to ensure __init_subclass__ is called + class GenericDiffSyncModel(DiffSyncModel): + pass + + return GenericDiffSyncModel() class ErrorProneModelMixin: @@ -86,11 +91,9 @@ class Site(DiffSyncModel): """Concrete DiffSyncModel subclass representing a site or location that contains devices.""" _modelname = "site" - _identifiers = ("name",) - _children = {"device": "devices"} - name: str - devices: List = [] + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + devices: Annotated[List, DiffSyncFieldType.CHILDREN, "device"] = [] @pytest.fixture @@ -110,14 +113,11 @@ class Device(DiffSyncModel): """Concrete DiffSyncModel subclass representing a device.""" _modelname = "device" - _identifiers = ("name",) - _attributes: ClassVar[Tuple[str, ...]] = ("role",) - _children = {"interface": "interfaces"} - name: str - site_name: Optional[str] # note this is not included in _attributes - role: str - interfaces: List = [] + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + site_name: Optional[str] # note this is not annotated with a field type + role: Annotated[str, DiffSyncFieldType.ATTRIBUTE] + interfaces: Annotated[List, DiffSyncFieldType.CHILDREN, "interface"] = [] @pytest.fixture @@ -135,15 +135,13 @@ class Interface(DiffSyncModel): """Concrete DiffSyncModel subclass representing an interface.""" _modelname = "interface" - _identifiers = ("device_name", "name") _shortname = ("name",) - _attributes = ("interface_type", "description") - device_name: str - name: str + device_name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] - interface_type: str = "ethernet" - description: Optional[str] + interface_type: Annotated[str, DiffSyncFieldType.ATTRIBUTE] = "ethernet" + description: Annotated[Optional[str], DiffSyncFieldType.ATTRIBUTE] @pytest.fixture @@ -167,9 +165,8 @@ class UnusedModel(DiffSyncModel): """Concrete DiffSyncModel subclass that can be referenced as a class attribute but never has any data.""" _modelname = "unused" - _identifiers = ("name",) - name: str + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] class GenericBackend(DiffSync): @@ -204,26 +201,21 @@ def load(self): class SiteA(Site): """Extend Site with a `people` list.""" - _children = {"device": "devices", "person": "people"} - - people: List = [] + people: Annotated[List, DiffSyncFieldType.CHILDREN, "person"] = [] class DeviceA(Device): """Extend Device with additional data fields.""" - _attributes = ("role", "tag") - - tag: str = "" + tag: Annotated[str, DiffSyncFieldType.ATTRIBUTE] = "" class PersonA(DiffSyncModel): """Concrete DiffSyncModel subclass representing a person; only used by BackendA.""" _modelname = "person" - _identifiers = ("name",) - name: str + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] class BackendA(GenericBackend): @@ -346,26 +338,21 @@ def exception_backend_a(): class SiteB(Site): """Extend Site with a `places` list.""" - _children = {"device": "devices", "place": "places"} - - places: List = [] + places: Annotated[List, DiffSyncFieldType.CHILDREN, "place"] = [] class DeviceB(Device): """Extend Device with a `vlans` list.""" - _attributes = ("role", "vlans") - - vlans: List = [] + vlans: Annotated[List, DiffSyncFieldType.ATTRIBUTE] = [] class PlaceB(DiffSyncModel): """Concrete DiffSyncModel subclass representing a place; only used by BackendB.""" _modelname = "place" - _identifiers = ("name",) - name: str + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] class BackendB(GenericBackend): diff --git a/tests/unit/test_diffsync_model.py b/tests/unit/test_diffsync_model.py index 90ecbc85..a1ee6152 100644 --- a/tests/unit/test_diffsync_model.py +++ b/tests/unit/test_diffsync_model.py @@ -15,12 +15,12 @@ limitations under the License. """ -from typing import List +from typing import List, Annotated import pytest from diffsync import DiffSyncModel -from diffsync.enum import DiffSyncModelFlags +from diffsync.enum import DiffSyncModelFlags, DiffSyncFieldType from diffsync.exceptions import ObjectStoreWrongType, ObjectAlreadyExists, ObjectNotFound from .conftest import Device, Interface @@ -260,16 +260,6 @@ def test_diffsync_model_subclass_validation(): # Pylint would complain because we're not actually using any of the classes declared below # pylint: disable=unused-variable - with pytest.raises(AttributeError) as excinfo: - - class BadIdentifier(DiffSyncModel): - """Model with an _identifiers referencing a nonexistent field.""" - - _identifiers = ("name",) - - assert "_identifiers" in str(excinfo.value) - assert "name" in str(excinfo.value) - with pytest.raises(AttributeError) as excinfo: class BadShortname(DiffSyncModel): @@ -283,78 +273,6 @@ class BadShortname(DiffSyncModel): assert "_shortname" in str(excinfo.value) assert "short_name" in str(excinfo.value) - with pytest.raises(AttributeError) as excinfo: - - class BadAttributes(DiffSyncModel): - """Model with _attributes referencing a nonexistent field.""" - - _identifiers = ("name",) - _shortname = ("short_name",) - _attributes = ("my_attr",) - - name: str - # Note that short_name doesn't have a type annotation - making sure this works too - short_name = "short_name" - - assert "_attributes" in str(excinfo.value) - assert "my_attr" in str(excinfo.value) - - with pytest.raises(AttributeError) as excinfo: - - class BadChildren(DiffSyncModel): - """Model with _children referencing a nonexistent field.""" - - _identifiers = ("name",) - _shortname = ("short_name",) - _attributes = ("my_attr",) - _children = {"device": "devices"} - - name: str - short_name = "short_name" - my_attr: int = 0 - - assert "_children" in str(excinfo.value) - assert "devices" in str(excinfo.value) - - with pytest.raises(AttributeError) as excinfo: - - class IdAttrOverlap(DiffSyncModel): - """Model including a field in both _identifiers and _attributes.""" - - _identifiers = ("name",) - _attributes = ("name",) - - name: str - - assert "both _identifiers and _attributes" in str(excinfo.value) - assert "name" in str(excinfo.value) - - with pytest.raises(AttributeError) as excinfo: - - class IdChildOverlap(DiffSyncModel): - """Model including a field in both _identifiers and _children.""" - - _identifiers = ("names",) - _children = {"name": "names"} - - names: str - - assert "both _identifiers and _children" in str(excinfo.value) - assert "names" in str(excinfo.value) - - with pytest.raises(AttributeError) as excinfo: - - class AttrChildOverlap(DiffSyncModel): - """Model including a field in both _attributes and _children.""" - - _attributes = ("devices",) - _children = {"device": "devices"} - - devices: List - - assert "both _attributes and _children" in str(excinfo.value) - assert "devices" in str(excinfo.value) - def test_diffsync_model_subclass_inheritance(): """Verify that the class validation works properly even with a hierarchy of subclasses.""" @@ -365,24 +283,19 @@ class Alpha(DiffSyncModel): """A model class representing a single Greek letter.""" _modelname = "alpha" - _identifiers = ("name",) _shortname = ("name",) - _attributes = ("letter",) - _children = {"number": "numbers"} - name: str - letter: str - numbers: List = [] + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + letter: Annotated[str, DiffSyncFieldType.ATTRIBUTE] + numbers: Annotated[List, DiffSyncFieldType.CHILDREN, "number"] = [] class Beta(Alpha): """A model class representing a single Greek letter in both English and Spanish.""" _modelname = "beta" - _identifiers = ("name", "nombre") # reference parent field, as well as a new field of our own - _attributes = ("letter", "letra") # reference parent field, as well as a new field of our own - nombre: str - letra: str + nombre: Annotated[str, DiffSyncFieldType.IDENTIFIER] + letra: Annotated[str, DiffSyncFieldType.ATTRIBUTE] beta = Beta(name="Beta", letter="β", nombre="Beta", letra="β") assert beta.get_unique_id() == "Beta__Beta" diff --git a/tests/unit/test_diffsync_model_flags.py b/tests/unit/test_diffsync_model_flags.py index 76457ccf..980c9d2b 100644 --- a/tests/unit/test_diffsync_model_flags.py +++ b/tests/unit/test_diffsync_model_flags.py @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. """ -from typing import List +from typing import List, Annotated import pytest from diffsync import DiffSync, DiffSyncModel -from diffsync.enum import DiffSyncModelFlags +from diffsync.enum import DiffSyncModelFlags, DiffSyncFieldType from diffsync.exceptions import ObjectNotFound @@ -121,9 +121,8 @@ def test_diffsync_diff_with_natural_deletion_order(): class TestModelChild(DiffSyncModel): # pylint: disable=missing-class-docstring _modelname = "child" - _identifiers = ("name",) - name: str + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] def delete(self): call_order.append(self.name) @@ -131,11 +130,9 @@ def delete(self): class TestModelParent(DiffSyncModel): # pylint: disable=missing-class-docstring _modelname = "parent" - _identifiers = ("name",) - _children = {"child": "children"} - name: str - children: List[TestModelChild] = [] + name: Annotated[str, DiffSyncFieldType.IDENTIFIER] + children: Annotated[List[TestModelChild], DiffSyncFieldType.CHILDREN, "child"] = [] def delete(self): call_order.append(self.name)