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
8 changes: 4 additions & 4 deletions .cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@
"project_with_config_settings": "no",
"generate_docs": "yes",
"version": "2.2.0",
"original_publish_year": "2025",
"original_publish_year": "2020",
"_drift_manager": {
"template": "git@github.com:networktocode-llc/cookiecutter-ntc.git",
"template": "https://github.com/networktocode-llc/cookiecutter-ntc.git",
"template_dir": "python",
"template_ref": "main",
"cookie_dir": "",
"pull_request_strategy": "create",
"pull_request_strategy": "update-or-create",
"post_actions": [],
"draft": false,
"baked_commit_ref": "bc789d65fa90182c0eb392664e1cba02ea187ab1",
"baked_commit_ref": "67d15ddeb638efb7c39ab746e97e7b9c96c16801",
"drift_managed_branch": "develop"
}
}
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,8 @@ jobs:
poetry-version: "2.1.3"
python-version: "3.13"
poetry-install-options: "--no-root"
- name: "Build Documentation"
run: "poetry run invoke build build-and-check-docs"
- name: "Run Poetry Build"
run: "poetry build"

- name: "Check that the release tag matches the version in pyproject.toml"
run: |
if [ "${{ github.ref_name }}" != "v$(poetry version -s)" ]; then exit 1; fi
Expand Down Expand Up @@ -58,17 +55,20 @@ jobs:
if: "startsWith(github.ref, 'refs/tags/v')"
needs: "build"
environment: "pypi"
# Steps to publish to PyPI.
steps:
- name: "Retrieve built package from cache"
uses: "actions/download-artifact@v4"
with:
name: "distfiles"
path: "dist/"
- name: "Publish package distribution to PyPI"
- name: "Publish package distributions to PyPI"
uses: "pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e" # v1.13.0
## Used for networktocode org since trusted publisher isn't supported for GitHub Plan.
with:
user: "__token__"
password: "${{ secrets.PYPI_API_TOKEN }}"
# End publish to PyPI job.

slack-notify:
needs:
Expand Down
1 change: 0 additions & 1 deletion changes/321.housekeeping

This file was deleted.

1 change: 0 additions & 1 deletion changes/323.housekeeping

This file was deleted.

8 changes: 7 additions & 1 deletion diffsync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,13 @@ def __init_subclass__(cls) -> None:

def __new__(cls, **kwargs): # type: ignore[no-untyped-def]
"""Document keyword arguments that were used to initialize Adapter."""
meta_kwargs = deepcopy(kwargs)
meta_kwargs = {}
for key, value in kwargs.items():
try:
meta_kwargs[key] = deepcopy(value)
except (TypeError, AttributeError):
# Some objects (e.g. Kafka Consumer, DB connections) cannot be deep copied
meta_kwargs[key] = value
instance = super().__new__(cls)
instance._meta_kwargs = meta_kwargs
return instance
Expand Down
11 changes: 11 additions & 0 deletions docs/admin/release_notes/version_2.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,14 @@ This document describes all new features and changes in the release. The format
## [v2.2.0] - 2025-12-08

Remove Python 3.9 support as it's EOL.

## [v2.2.1 (2026-02-12)](https://github.com/networktocode/diffsync/releases/tag/v2.2.1)

### Fixed

- [#334](https://github.com/networktocode/diffsync/issues/334) - Fix TypeError being thrown when non-serializable object is used in creating a Diff/Adapter.

### Housekeeping

- [#321](https://github.com/networktocode/diffsync/issues/321) - Fixed CI release workflow.
- Rebaked from the cookie `main`.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "diffsync"
version = "2.2.0"
version = "2.2.1"
description = "Library to easily sync/diff/update 2 different data sources"
authors = ["Network to Code, LLC <info@networktocode.com>"]
license = "Apache-2.0"
Expand Down
85 changes: 85 additions & 0 deletions tests/unit/test_diffsync.py
Original file line number Diff line number Diff line change
Expand Up @@ -1183,3 +1183,88 @@ def test_adapter_new_independent_instances():
assert adapter1._meta_kwargs["internal_storage_engine"] == LocalStore # pylint: disable=protected-access
assert adapter2._meta_kwargs["name"] == "adapter2" # pylint: disable=protected-access
assert adapter2._meta_kwargs["internal_storage_engine"] == LocalStore # pylint: disable=protected-access


class _AdapterWithExtraKwargs(Adapter):
"""Minimal Adapter subclass that accepts extra kwargs for testing __new__ serialization."""

def __init__(self, name=None, internal_storage_engine=LocalStore, **kwargs):
super().__init__(name=name, internal_storage_engine=internal_storage_engine)


def test_adapter_new_serializable_objects_are_deep_copied():
"""Test that serializable objects passed to __new__ are deep-copied into _meta_kwargs."""
mutable_config = {"host": "localhost", "port": 5432}
mutable_list = [1, 2, 3]
adapter = _AdapterWithExtraKwargs(
name="test",
config=mutable_config,
tags=mutable_list,
internal_storage_engine=LocalStore,
)

# Verify values are stored
assert adapter._meta_kwargs["config"] == {"host": "localhost", "port": 5432} # pylint: disable=protected-access
assert adapter._meta_kwargs["tags"] == [1, 2, 3] # pylint: disable=protected-access

# Mutate the original objects - _meta_kwargs should retain the original values (deep copy)
mutable_config["port"] = 9999
mutable_list.append(4)

assert adapter._meta_kwargs["config"] == {"host": "localhost", "port": 5432} # pylint: disable=protected-access
assert adapter._meta_kwargs["tags"] == [1, 2, 3] # pylint: disable=protected-access


def test_adapter_new_non_serializable_type_error_stored_as_is():
"""Test that objects raising TypeError on deepcopy are stored as-is in _meta_kwargs."""

class NonCopyableTypeError:
"""Object that raises TypeError when deep-copied (e.g. DB connection, Kafka Consumer)."""

def __deepcopy__(self, memo=None):
raise TypeError("Cannot deep copy this object")

non_copyable = NonCopyableTypeError()
adapter = _AdapterWithExtraKwargs(name="test", non_copyable=non_copyable, internal_storage_engine=LocalStore)

assert adapter._meta_kwargs["non_copyable"] is non_copyable # pylint: disable=protected-access


def test_adapter_new_non_serializable_attribute_error_stored_as_is():
"""Test that objects raising AttributeError on deepcopy are stored as-is in _meta_kwargs."""

class NonCopyableAttributeError:
"""Object that raises AttributeError when deep-copied."""

def __deepcopy__(self, memo=None):
raise AttributeError("Cannot deep copy - missing attribute")

non_copyable = NonCopyableAttributeError()
adapter = _AdapterWithExtraKwargs(name="test", non_copyable=non_copyable, internal_storage_engine=LocalStore)

assert adapter._meta_kwargs["non_copyable"] is non_copyable # pylint: disable=protected-access


def test_adapter_new_mixed_serializable_and_non_serializable_kwargs():
"""Test that __new__ handles mix of serializable and non-serializable kwargs correctly."""

class NonCopyable:
def __deepcopy__(self, memo=None):
raise TypeError("Cannot copy")

serializable_dict = {"key": "value"}
non_copyable = NonCopyable()

adapter = _AdapterWithExtraKwargs(
name="test",
config=serializable_dict,
connection=non_copyable,
internal_storage_engine=LocalStore,
)

# Serializable: deep-copied (independent copy)
assert adapter._meta_kwargs["config"] == {"key": "value"} # pylint: disable=protected-access
assert adapter._meta_kwargs["config"] is not serializable_dict

# Non-serializable: stored by reference
assert adapter._meta_kwargs["connection"] is non_copyable # pylint: disable=protected-access
Loading