From 4d7b0f5d49287d89433ac323111753a224dbcd25 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:53:53 -0600 Subject: [PATCH 1/7] Bump prepatch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7b44b86..115b43d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "diffsync" -version = "2.2.0" +version = "2.2.1a0" description = "Library to easily sync/diff/update 2 different data sources" authors = ["Network to Code, LLC "] license = "Apache-2.0" From cab4270aab0393dca2e23f2441bea9d80207a0c1 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 12 Dec 2025 07:58:15 -0600 Subject: [PATCH 2/7] Fix CI (#321) --- .github/workflows/release.yml | 2 +- changes/321.housekeeping | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changes/321.housekeeping diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee870479..bdb33f71 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: python-version: "3.13" poetry-install-options: "--no-root" - name: "Build Documentation" - run: "poetry run invoke build-and-check-docs" + run: "poetry run invoke build build-and-check-docs" - name: "Run Poetry Build" run: "poetry build" diff --git a/changes/321.housekeeping b/changes/321.housekeeping new file mode 100644 index 00000000..044f8618 --- /dev/null +++ b/changes/321.housekeeping @@ -0,0 +1 @@ +Fixed CI release workflow. From 3d614ca18699da7fab5c08d17e56e95332a65b57 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:10:55 -0600 Subject: [PATCH 3/7] Bump prepatch --- changes/321.housekeeping | 1 - changes/323.housekeeping | 1 - pyproject.toml | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 changes/321.housekeeping delete mode 100644 changes/323.housekeeping diff --git a/changes/321.housekeeping b/changes/321.housekeeping deleted file mode 100644 index 044f8618..00000000 --- a/changes/321.housekeeping +++ /dev/null @@ -1 +0,0 @@ -Fixed CI release workflow. diff --git a/changes/323.housekeeping b/changes/323.housekeeping deleted file mode 100644 index 6a862e0f..00000000 --- a/changes/323.housekeeping +++ /dev/null @@ -1 +0,0 @@ -Reverted PyPI publishing method. diff --git a/pyproject.toml b/pyproject.toml index b7b44b86..115b43d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "diffsync" -version = "2.2.0" +version = "2.2.1a0" description = "Library to easily sync/diff/update 2 different data sources" authors = ["Network to Code, LLC "] license = "Apache-2.0" From 0cf767037621f9aff5e2d60c9b0bfae4e54a4b64 Mon Sep 17 00:00:00 2001 From: "Network to Code (smith-ntc)" <35795917+smith-ntc@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:07:27 -0800 Subject: [PATCH 4/7] Cookie updated targeting develop by NetworkToCode Cookie Drift Manager Tool (#330) * Cookie updated targeting develop by NetworkToCode Cookie Drift Manager Tool Template: ``` { "template": "https://github.com/networktocode-llc/cookiecutter-ntc.git", "dir": "python", "ref": "main", "path": "/__w/cookiecutter-nautobot-app-drift-manager/cookiecutter-nautobot-app-drift-manager/cookiecutter-ntc/python" } ``` Cookie: ``` { "remote": "https://github.com/networktocode/diffsync.git", "path": "/tmp/tmpn08h9047/diffsync", "repository_path": "/tmp/tmpn08h9047/diffsync", "dir": "", "branch_prefix": "drift-manager/develop", "context": { "codeowner_github_usernames": "@glennmatthews @michalis1 @jdrew82", "full_name": "Network to Code, LLC", "email": "info@networktocode.com", "github_org": "networktocode", "description": "Library to easily sync/diff/update 2 different data sources", "project_name": "diffsync", "project_slug": "diffsync", "repo_url": "https://github.com/networktocode/diffsync", "base_url": "diffsync", "project_python_name": "diffsync", "project_python_base_version": "3.10", "project_with_config_settings": "no", "generate_docs": "yes", "version": "2.2.0", "original_publish_year": "2025", "_template": "/__w/cookiecutter-nautobot-app-drift-manager/cookiecutter-nautobot-app-drift-manager/cookiecutter-ntc/python", "_output_dir": "/tmp/tmpn08h9047", "_repo_dir": "/__w/cookiecutter-nautobot-app-drift-manager/cookiecutter-nautobot-app-drift-manager/cookiecutter-ntc/python", "_checkout": null }, "drift_managed_branch": "develop", "remote_name": "origin", "pull_request_strategy": "PullRequestStrategy.UPDATE_OR_CREATE", "post_actions": [], "baked_commit_ref": "bc789d65fa90182c0eb392664e1cba02ea187ab1", "draft": false } ``` CLI Arguments: ``` { "cookie_dir": "", "input": false, "json_filename": "", "output_dir": "", "push": true, "template": "./cookiecutter-ntc", "template_dir": "python", "template_ref": "main", "pull_request": "update-or-create", "post_action": [], "disable_post_actions": true, "draft": null, "drift_managed_branch": "develop" } ``` * fix merge conflicts --------- Co-authored-by: bakebot Co-authored-by: Jeff Kala --- .cookiecutter.json | 8 ++++---- .github/workflows/release.yml | 8 ++++---- changes/+main.housekeeping | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 changes/+main.housekeeping diff --git a/.cookiecutter.json b/.cookiecutter.json index c8768232..3a74a3a0 100644 --- a/.cookiecutter.json +++ b/.cookiecutter.json @@ -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" } } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99b60bd9..ef26e629 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -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: diff --git a/changes/+main.housekeeping b/changes/+main.housekeeping new file mode 100644 index 00000000..3433adf6 --- /dev/null +++ b/changes/+main.housekeeping @@ -0,0 +1 @@ +Rebaked from the cookie `main`. From 5620c418d93c65a5eef203035d48422bdec3e25b Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:23:44 -0600 Subject: [PATCH 5/7] Fix Deepcopy Bug (#335) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Tweak deepcopy of kwargs to handle non-serializable objects. * test: ✅ Add tests validating handling of serialized and non-serialized objects * docs: Add changelog fragment --- changes/334.fixed | 1 + diffsync/__init__.py | 8 +++- tests/unit/test_diffsync.py | 85 +++++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 changes/334.fixed diff --git a/changes/334.fixed b/changes/334.fixed new file mode 100644 index 00000000..5578d075 --- /dev/null +++ b/changes/334.fixed @@ -0,0 +1 @@ +Fix TypeError being thrown when non-serializable object is used in creating a Diff/Adapter. diff --git a/diffsync/__init__.py b/diffsync/__init__.py index b39a6b58..c48e7053 100644 --- a/diffsync/__init__.py +++ b/diffsync/__init__.py @@ -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 diff --git a/tests/unit/test_diffsync.py b/tests/unit/test_diffsync.py index 924411fe..26a2f1cb 100644 --- a/tests/unit/test_diffsync.py +++ b/tests/unit/test_diffsync.py @@ -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 From b6982fb7e7ebb404073e11637368ee5eedb2f69a Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:34:17 -0600 Subject: [PATCH 6/7] docs: Update release notes --- changes/+main.housekeeping | 1 - changes/321.housekeeping | 1 - changes/334.fixed | 1 - docs/admin/release_notes/version_2.2.md | 11 +++++++++++ 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 changes/+main.housekeeping delete mode 100644 changes/321.housekeeping delete mode 100644 changes/334.fixed diff --git a/changes/+main.housekeeping b/changes/+main.housekeeping deleted file mode 100644 index 3433adf6..00000000 --- a/changes/+main.housekeeping +++ /dev/null @@ -1 +0,0 @@ -Rebaked from the cookie `main`. diff --git a/changes/321.housekeeping b/changes/321.housekeeping deleted file mode 100644 index 044f8618..00000000 --- a/changes/321.housekeeping +++ /dev/null @@ -1 +0,0 @@ -Fixed CI release workflow. diff --git a/changes/334.fixed b/changes/334.fixed deleted file mode 100644 index 5578d075..00000000 --- a/changes/334.fixed +++ /dev/null @@ -1 +0,0 @@ -Fix TypeError being thrown when non-serializable object is used in creating a Diff/Adapter. diff --git a/docs/admin/release_notes/version_2.2.md b/docs/admin/release_notes/version_2.2.md index 405c5742..f2d84d97 100644 --- a/docs/admin/release_notes/version_2.2.md +++ b/docs/admin/release_notes/version_2.2.md @@ -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`. From a292361364e055f38cfd4ca3d51de61cb84701c5 Mon Sep 17 00:00:00 2001 From: Justin Drew <2396364+jdrew82@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:34:34 -0600 Subject: [PATCH 7/7] build: Bump patch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 115b43d2..20a82c8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "diffsync" -version = "2.2.1a0" +version = "2.2.1" description = "Library to easily sync/diff/update 2 different data sources" authors = ["Network to Code, LLC "] license = "Apache-2.0"