Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make an empty DiffSync instance evaluate to True. #114

Closed
wants to merge 10 commits into from
Closed
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
1 change: 1 addition & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# E501: Line length is enforced by Black, so flake8 doesn't need to check it
# W503: Black disagrees with this rule, as does PEP 8; Black wins
ignore = E501, W503
exclude = .venv
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,18 @@ jobs:
fail-fast: true
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
poetry-version: ["1.5.1"]
runs-on: "ubuntu-20.04"
env:
PYTHON_VER: "${{ matrix.python-version }}"
steps:
- name: "Check out repository code"
uses: "actions/checkout@v2"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v5"
uses: "networktocode/gh-action-setup-poetry-environment@3ea5d3ecf382cdcb0c74d4c0ff0629d95fce63c7"
with:
python-version: "${{ matrix.python-version }}"
poetry-version: "${{ matrix.poetry-version }}"
- name: "Install redis"
run: "sudo apt-get install -y redis"
- name: "Run poetry Install"
Expand Down
40 changes: 25 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Changelog

## v1.8.0 - 2023-04-18
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Changed

- **BREAKING CHANGE** #236/240 - Upgrade to Pydantic v2.

## [1.8.0] - 2023-04-18

### Added

Expand All @@ -13,7 +23,7 @@
- #77/#188 - `sync_from()` and `sync_to()` now return the `Diff` that was applied.
- #211 - Loosened `packaging` and `structlog` library dependency constraints for broader compatibility.

## v1.7.0 - 2022-11-03
## [1.7.0] - 2022-11-03

### Changed

Expand All @@ -31,15 +41,15 @@

### Fixed

- #149 Limit redundant CI concurrency
- #149 - Limit redundant CI concurrency

## v1.6.0 - 2022-07-09
## [1.6.0] - 2022-07-09

### Changed

- #120 - Dropped support for Python 3.6, new minimum is Python 3.7

## v1.5.1 - 2022-06-30
## [1.5.1] - 2022-06-30

### Added

Expand All @@ -54,13 +64,13 @@
- #115 - Fixed ReadTheDocs rendering pipeline
- #118 - Fixed a regression in `DiffSync.get(modelname, identifiers)` introduced in 1.5.0

## v1.5.0 - 2022-06-07
## [1.5.0] - 2022-06-07

### Added

- #106 - Add a new, optional, backend store based in Redis

## v1.4.3 - 2022-03-03
## [1.4.3] - 2022-03-03

### Fixed

Expand All @@ -70,25 +80,25 @@

### Changed

- #103 Update development dependencies
- #103 - Update development dependencies

## v1.4.2 - 2022-02-28
## [1.4.2] - 2022-02-28

**WARNING** - #90 inadvertently introduced a breaking API change in DiffSync 1.4.0 through 1.4.2 (#101); this change was reverted in #102 for DiffSync 1.4.3 and later. We recommend not using this release, and moving to 1.4.3 instead.

### Fixed

- #100 - Added explicit dependency on `packaging`.

## v1.4.1 - 2022-01-26
## [1.4.1] - 2022-01-26

**WARNING** - #90 inadvertently introduced a breaking API change in DiffSync 1.4.0 through 1.4.2 (#101); this change was reverted in #102 for DiffSync 1.4.3 and later. We recommend not using this release, and moving to 1.4.3 instead.

### Fixed

- #95 - Removed optional dependencies on `sphinx`, `m2r2`, `sphinx-rtd-theme`, `toml`.

## v1.4.0 - 2022-01-24
## [1.4.0] - 2022-01-24

**WARNING** - #90 inadvertently introduced a breaking API change in DiffSync 1.4.0 through 1.4.2 (#101); this change was reverted in #102 for DiffSync 1.4.3 and later. We recommend not using this release, and moving to 1.4.3 instead.

Expand Down Expand Up @@ -117,19 +127,19 @@
- #51 - Update minimum Pydantic version due to security advisory GHSA-5jqp-qgf6-3pvh
- #63 - Fix type in Readme

## v1.3.0 - 2021-04-07
## [1.3.0] - 2021-04-07

### Added

- #48 - added optional `callback` argument to `diff_from`/`diff_to`/`sync_from`/`sync_to` for use with progress reporting.

## v1.2.0 - 2020-12-08
## [1.2.0] - 2020-12-08

### Added

- #45 - minimum Python version lowered from 3.7 to 3.6, also now tested against Python 3.9.

## v1.1.0 - 2020-12-01
## [1.1.0] - 2020-12-01

### Added

Expand All @@ -147,6 +157,6 @@

- #44 - On CRUD failure, do not generate an extraneous "success" log message in addition to the "failed" message

## v1.0.0 - 2020-10-23
## [1.0.0] - 2020-10-23

Initial release
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --upgrade pip \
&& pip install poetry
&& pip install poetry==1.5.1


WORKDIR /local
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ DiffSync is at its most useful when you have multiple sources or sets of data to

# Overview of DiffSync

DiffSync acts as an intermediate translation layer between all of the data sets you are diffing and/or syncing. In practical terms, this means that to use DiffSync, you will define a set of data models as well as the “adapters” needed to translate between each base data source and the data model. In Python terms, the adapters will be subclasses of the `DiffSync` class, and each data model class will be a subclass of the `DiffSyncModel` class.
DiffSync acts as an intermediate translation layer between all of the data sets you are diffing and/or syncing. In practical terms, this means that to use DiffSync, you will define a set of data models as well as the “adapters” needed to translate between each base data source and the data model. In Python terms, the adapters will be subclasses of the `Adapter` class, and each data model class will be a subclass of the `DiffSyncModel` class.

![Diffsync Components](https://raw.githubusercontent.com/networktocode/diffsync/develop/docs/images/diffsync_components.png "Diffsync Components")

Expand Down
55 changes: 30 additions & 25 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 @@ -91,39 +91,34 @@ class DiffSyncModel(BaseModel):
Can be set as a class attribute or an instance attribute as needed.
"""

diffsync: Optional["DiffSync"] = None
diffsync: Optional["Adapter"] = None
"""Optional: the DiffSync instance that owns this model instance."""

_status: DiffSyncStatus = PrivateAttr(DiffSyncStatus.SUCCESS)
"""Status of the last attempt at creating/updating/deleting this model."""

_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 @@ -183,7 +178,7 @@ def set_status(self, status: DiffSyncStatus, message: StrType = "") -> None:
self._status_message = message

@classmethod
def create_base(cls, diffsync: "DiffSync", ids: Dict, attrs: Dict) -> Optional[Self]:
def create_base(cls, diffsync: "Adapter", ids: Dict, attrs: Dict) -> Optional[Self]:
"""Instantiate this class, along with any platform-specific data creation.

This method is not meant to be subclassed, users should redefine create() instead.
Expand All @@ -201,7 +196,7 @@ def create_base(cls, diffsync: "DiffSync", ids: Dict, attrs: Dict) -> Optional[S
return model

@classmethod
def create(cls, diffsync: "DiffSync", ids: Dict, attrs: Dict) -> Optional[Self]:
def create(cls, diffsync: "Adapter", ids: Dict, attrs: Dict) -> Optional[Self]:
"""Instantiate this class, along with any platform-specific data creation.

Subclasses must call `super().create()` or `self.create_base()`; they may wish to then override the default status information
Expand Down Expand Up @@ -402,7 +397,7 @@ def remove_child(self, child: "DiffSyncModel") -> None:
childs.remove(child.get_unique_id())


class DiffSync: # pylint: disable=too-many-public-methods
class Adapter: # pylint: disable=too-many-public-methods
"""Class for storing a group of DiffSyncModel instances and diffing/synchronizing to another DiffSync instance."""

# In any subclass, you would add mapping of names to specific model classes here:
Expand Down Expand Up @@ -464,6 +459,13 @@ def __str__(self) -> StrType:
def __repr__(self) -> StrType:
return f"<{str(self)}>"

def __bool__(self) -> bool:
"""Always evaluate DiffSync instances as True.

This is needed because without it the __len__ method would be used, which in turn would cause empty DiffSync
instances to evaluate as False."""
return True

def __len__(self) -> int:
"""Total number of elements stored."""
return self.store.count()
Expand Down Expand Up @@ -535,7 +537,7 @@ def load_from_dict(self, data: Dict) -> None:

def sync_from( # pylint: disable=too-many-arguments
self,
source: "DiffSync",
source: "Adapter",
diff_class: Type[Diff] = Diff,
flags: DiffSyncFlags = DiffSyncFlags.NONE,
callback: Optional[Callable[[StrType, int, int], None]] = None,
Expand Down Expand Up @@ -573,7 +575,7 @@ def sync_from( # pylint: disable=too-many-arguments

def sync_to( # pylint: disable=too-many-arguments
self,
target: "DiffSync",
target: "Adapter",
diff_class: Type[Diff] = Diff,
flags: DiffSyncFlags = DiffSyncFlags.NONE,
callback: Optional[Callable[[StrType, int, int], None]] = None,
Expand All @@ -597,7 +599,7 @@ def sync_to( # pylint: disable=too-many-arguments

def sync_complete(
self,
source: "DiffSync",
source: "Adapter",
diff: Diff,
flags: DiffSyncFlags = DiffSyncFlags.NONE,
logger: Optional[structlog.BoundLogger] = None,
Expand All @@ -623,7 +625,7 @@ def sync_complete(

def diff_from(
self,
source: "DiffSync",
source: "Adapter",
diff_class: Type[Diff] = Diff,
flags: DiffSyncFlags = DiffSyncFlags.NONE,
callback: Optional[Callable[[StrType, int, int], None]] = None,
Expand All @@ -644,7 +646,7 @@ def diff_from(

def diff_to(
self,
target: "DiffSync",
target: "Adapter",
diff_class: Type[Diff] = Diff,
flags: DiffSyncFlags = DiffSyncFlags.NONE,
callback: Optional[Callable[[StrType, int, int], None]] = None,
Expand Down Expand Up @@ -854,5 +856,8 @@ def count(self, model: Union[StrType, "DiffSyncModel", Type["DiffSyncModel"], No
return self.store.count(model=model)


# For backwards-compatibility, keep around the old name
DiffSync = Adapter

# DiffSyncModel references DiffSync and DiffSync references DiffSyncModel. Break the typing loop:
DiffSyncModel.update_forward_refs()
DiffSyncModel.model_rebuild()
10 changes: 5 additions & 5 deletions diffsync/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

if TYPE_CHECKING: # pragma: no cover
# For type annotation purposes, we have a circular import loop between __init__.py and this file.
from . import DiffSync, DiffSyncModel # pylint: disable=cyclic-import
from . import Adapter, DiffSyncModel # pylint: disable=cyclic-import


class DiffSyncDiffer: # pylint: disable=too-many-instance-attributes
Expand All @@ -37,8 +37,8 @@ class DiffSyncDiffer: # pylint: disable=too-many-instance-attributes

def __init__( # pylint: disable=too-many-arguments
self,
src_diffsync: "DiffSync",
dst_diffsync: "DiffSync",
src_diffsync: "Adapter",
dst_diffsync: "Adapter",
flags: DiffSyncFlags,
diff_class: Type[Diff] = Diff,
callback: Optional[Callable[[str, int, int], None]] = None,
Expand Down Expand Up @@ -288,8 +288,8 @@ class DiffSyncSyncer: # pylint: disable=too-many-instance-attributes
def __init__( # pylint: disable=too-many-arguments
self,
diff: Diff,
src_diffsync: "DiffSync",
dst_diffsync: "DiffSync",
src_diffsync: "Adapter",
dst_diffsync: "Adapter",
flags: DiffSyncFlags,
callback: Optional[Callable[[str, int, int], None]] = None,
):
Expand Down
4 changes: 2 additions & 2 deletions diffsync/store/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

if TYPE_CHECKING:
from diffsync import DiffSyncModel
from diffsync import DiffSync
from diffsync import Adapter


class BaseStore:
Expand All @@ -15,7 +15,7 @@ class BaseStore:
def __init__(
self, # pylint: disable=unused-argument
*args: Any, # pylint: disable=unused-argument
diffsync: Optional["DiffSync"] = None,
diffsync: Optional["Adapter"] = None,
name: str = "",
**kwargs: Any, # pylint: disable=unused-argument
) -> None:
Expand Down
Loading
Loading