Migration to Pydantic v2 by Kircheneer · Pull Request #240 · networktocode/diffsync · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions CHANGELOG.md
27 changes: 11 additions & 16 deletions diffsync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from typing import Callable, ClassVar, Dict, List, Optional, Tuple, Type, Union, Any, Set
from typing_extensions import Self

from pydantic import BaseModel, PrivateAttr
from pydantic import ConfigDict, BaseModel, PrivateAttr
import structlog # type: ignore

from diffsync.diff import Diff
Expand Down Expand Up @@ -99,31 +99,26 @@ class DiffSyncModel(BaseModel):

_status_message: str = PrivateAttr("")
"""Message, if any, associated with the create/update/delete status value."""
model_config = ConfigDict(arbitrary_types_allowed=True)

class Config: # pylint: disable=too-few-public-methods
"""Pydantic class configuration."""

# Let us have a DiffSync as an instance variable even though DiffSync is not a Pydantic model itself.
arbitrary_types_allowed = True

def __init_subclass__(cls) -> None:
@classmethod
def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
"""Validate that the various class attribute declarations correspond to actual instance fields.

Called automatically on subclass declaration.
"""
variables = cls.__fields__.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):
if attr not in cls.model_fields 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 cls.model_fields:
raise AttributeError(f"_shortname {cls._shortname} references missing or un-annotated attr {attr}")
for attr in cls._attributes:
if attr not in variables:
if attr not in cls.model_fields:
raise AttributeError(f"_attributes {cls._attributes} references missing or un-annotated attr {attr}")
for attr in cls._children.values():
if attr not in variables:
if attr not in cls.model_fields:
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)
Expand All @@ -147,15 +142,15 @@ def dict(self, **kwargs: Any) -> Dict:
"""Convert this DiffSyncModel to a dict, excluding the diffsync field by default as it is not serializable."""
if "exclude" not in kwargs:
kwargs["exclude"] = {"diffsync"}
return super().dict(**kwargs)
return super().model_dump(**kwargs)

def json(self, **kwargs: Any) -> StrType:
"""Convert this DiffSyncModel to a JSON string, excluding the diffsync field by default as it is not serializable."""
if "exclude" not in kwargs:
kwargs["exclude"] = {"diffsync"}
if "exclude_defaults" not in kwargs:
kwargs["exclude_defaults"] = True
return super().json(**kwargs)
return super().model_dump_json(**kwargs)

def str(self, include_children: bool = True, indent: int = 0) -> StrType:
"""Build a detailed string representation of this DiffSyncModel and optionally its children."""
Expand Down Expand Up @@ -855,4 +850,4 @@ def count(self, model: Union[StrType, "DiffSyncModel", Type["DiffSyncModel"], No


# DiffSyncModel references DiffSync and DiffSync references DiffSyncModel. Break the typing loop:
DiffSyncModel.update_forward_refs()
DiffSyncModel.model_rebuild()
6 changes: 3 additions & 3 deletions docs/source/getting_started/index.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
###############
Getting Started
###############
#########
Upgrading
#########

.. mdinclude:: 01-getting-started.md
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Welcome to DiffSync's documentation!
getting_started/index
core_engine/index
examples/index
upgrading/index
api/diffsync
license/index

Expand Down
31 changes: 31 additions & 0 deletions docs/source/upgrading/01-upgrading-to-2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Upgrading to 2.0

With diffsync 2.0, there a couple of breaking changes. What they are and how to deal with them is described in this document.

## Upgrade to Pydantic's major version 2

A [migration guide](https://docs.pydantic.dev/latest/migration/) is available in the Pydantic documentation. Here are the key things that may apply to your usage of diffsync:

- Any fields that are of type `Optional` now need to provide an explicit `None` default (you can use [bump-pydantic](https://github.com/pydantic/bump-pydantic) to deal with this automatically for the most part)

```python
from typing import Optional

from diffsync import DiffSyncModel

# Before
class Person(DiffSyncModel):
_identifiers = ("name",)
_attributes = ("age",)

name: str
age: Optional[int]

# After
class BetterPerson(DiffSyncModel)
_identifiers = ("name",)
_attributes = ("age",)

name: str
age: Optional[int] = None
```
5 changes: 5 additions & 0 deletions docs/source/upgrading/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#########
Upgrading
#########

.. mdinclude:: 01-upgrading-to-2.0.md
6 changes: 3 additions & 3 deletions examples/01-multiple-data-sources/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ class Device(DiffSyncModel):
_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
site_name: Optional[str] = None # note that this attribute is NOT included in _attributes
role: Optional[str] = None # note that this attribute is NOT included in _attributes
interfaces: List = []


Expand All @@ -56,4 +56,4 @@ class Interface(DiffSyncModel):
name: str
device_name: str

description: Optional[str]
description: Optional[str] = None
2 changes: 1 addition & 1 deletion examples/03-remote-system/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ class Country(DiffSyncModel):
slug: str
name: str
region: str
population: Optional[int]
population: Optional[int] = 0
6 changes: 3 additions & 3 deletions examples/04-get-update-instantiate/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ class Device(DiffSyncModel):
_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
site_name: Optional[str] = None # note that this attribute is NOT included in _attributes
role: Optional[str] = None # note that this attribute is NOT included in _attributes
interfaces: List = []
sites: List = []

Expand All @@ -55,4 +55,4 @@ class Interface(DiffSyncModel):
name: str
device_name: str

description: Optional[str]
description: Optional[str] = None
12 changes: 6 additions & 6 deletions examples/05-nautobot-peeringdb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class RegionModel(DiffSyncModel):
# Data type declarations for all identifiers and attributes
name: str
slug: str
description: Optional[str]
parent_name: Optional[str] # may be None
description: Optional[str] = None
parent_name: Optional[str] = None
sites: List = []

# Not in _attributes or _identifiers, hence not included in diff calculations
Expand All @@ -49,10 +49,10 @@ class SiteModel(DiffSyncModel):
name: str
slug: str
status_slug: str
region_name: Optional[str] # may be None
description: Optional[str]
latitude: Optional[float]
longitude: Optional[float]
region_name: Optional[str] = None
description: Optional[str] = None
latitude: Optional[float] = None
longitude: Optional[float] = None

# Not in _attributes or _identifiers, hence not included in diff calculations
pk: Optional[Union[UUID, int]]
6 changes: 3 additions & 3 deletions examples/06-ip-prefixes/models.py
Loading