From fd7e948a390afed6def2e827abc0b8e1505bb2dc Mon Sep 17 00:00:00 2001 From: James Prior Date: Thu, 25 Jun 2026 12:34:27 +0100 Subject: [PATCH 1/4] Add `JSONPatch.patch` and clarify docs --- CHANGELOG.md | 4 +++ README.md | 6 ++-- docs/quickstart.md | 38 ++++++++++++++++++++++++ jsonpath/__about__.py | 2 +- jsonpath/patch.py | 62 ++++++++++++++++++++++++++++++++++++---- tests/cts | 2 +- tests/test_json_patch.py | 39 +++++++++++++++++++++++++ 7 files changed, 143 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6425c0..9be37c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Python JSONPath Change Log +## Version 2.1.0 + +Added `JSONPatch.patch(data)` that performs a deep copy of `data` before applying the patch. In contrast, `JSONPatch.apply(data)` does not perform a deep copy, always modifying `data` in place, even if a patch operation fails. See [#129](https://github.com/jg-rp/python-jsonpath/issues/129). + ## Version 2.0.2 **Fixes** diff --git a/README.md b/README.md index c8ffe28..78d4440 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,10 @@ print(jane_score) # 55 ### JSON Patch -We also include an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) compliant implementation of JSON Patch. See JSON Patch [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#patchapplypatch-data) and [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch) +We also include an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) compliant implementation of JSON Patch. See JSON Patch [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#patchapplypatch-data) and the [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch). + +> [!NOTE] +> Note that objects passed to `patch.apply()` and `JSONPatch.apply()` are modified in place, even if a patch operation fails. ```python from jsonpath import patch @@ -134,7 +137,6 @@ patch_operations = [ data = {"some": {"other": "thing"}} patch.apply(patch_operations, data) print(data) # {'some': {'other': 'thing', 'foo': {'bar': [1], 'else': 'thing'}}} - ``` ## License diff --git a/docs/quickstart.md b/docs/quickstart.md index 449ee35..b82721d 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -272,6 +272,10 @@ _patch_ can be a string or file-like object containing a valid JSON Patch docume _data_ is the target JSON document to modify. If _data_ is a string or file-like object, it will be loaded with _json.loads_. Otherwise _data_ should be a JSON-like data structure and will be **modified in place**. +!!! warning + + Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `JSONPatch.patch()`, which performs a deep copy for you. + ```python from jsonpath import patch @@ -324,6 +328,40 @@ patch.apply(data) print(data) # {'some': {'other': 'thing', 'foo': {'bar': [1], 'else': 'thing'}}} ``` +## `JSONPatch.patch(data)` + +Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `JSONPatch.patch()`, which performs a deep copy for you. + +!!! note + + `JSONPatch.patch(data)` accepts JSON-like like dictionaries and lists, whereas `JSONPatch.apply(data)` accepts file-like objects, JSON-formatted strings or JSON-like data. + +```python +import copy +from typing import Any + +from jsonpath import JSONPatch +from jsonpath import JSONPatchError + +patcher = JSONPatch( + [ + {"op": "replace", "path": "/a/b/c", "value": 42}, + {"op": "test", "path": "/a/b/c", "value": "C"}, # Always fails + ] +) + +data: dict[str, Any] = {"a": {"b": {"c": 1}}} +data_ = copy.deepcopy(data) + +try: + patcher.patch(data) +except JSONPatchError: + # TODO: something + pass + +assert data == data_ +``` + ## What's Next? Read about the [Query Iterators](query.md) API or [user-defined filter functions](advanced.md#function-extensions). Also see how to make extra data available to filters with [Extra Filter Context](advanced.md#filter-variables). diff --git a/jsonpath/__about__.py b/jsonpath/__about__.py index e856d07..d570b96 100644 --- a/jsonpath/__about__.py +++ b/jsonpath/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2023-present James Prior # # SPDX-License-Identifier: MIT -__version__ = "2.0.2" +__version__ = "2.1.0" diff --git a/jsonpath/patch.py b/jsonpath/patch.py index 54480e7..a93820f 100644 --- a/jsonpath/patch.py +++ b/jsonpath/patch.py @@ -71,7 +71,7 @@ def apply( if target == "-": parent.append(self.value) else: - index = self.path._index(target) # noqa: SLF001 + index = self.path._index(target) # type: ignore # noqa: SLF001 if index == len(parent): parent.append(self.value) else: @@ -303,12 +303,12 @@ def apply( self, data: Union[MutableSequence[object], MutableMapping[str, object]] ) -> Union[MutableSequence[object], MutableMapping[str, object]]: """Apply this patch operation to _data_.""" - source_parent, source_obj = self.source.resolve_parent(data) + _, source_obj = self.source.resolve_parent(data) if source_obj is UNDEFINED: raise JSONPatchError("source object does not exist") - dest_parent, dest_obj = self.dest.resolve_parent(data) + dest_parent, _ = self.dest.resolve_parent(data) if dest_parent is None: # Copy source to root @@ -639,7 +639,7 @@ def apply( If _data_ is a string or file-like object, it will be loaded with _json.loads_. Otherwise _data_ should be a JSON-like data structure and - will be modified in place. + will be modified in place, even if a patch operation fails. When modifying _data_ in place, we return modified data too. This is to allow for replacing _data's_ root element, which is allowed by some @@ -674,6 +674,25 @@ def apply( return _data + def patch( + self, + data: Union[MutableSequence[Any], MutableMapping[str, Any]], + ) -> object: + """Apply all operations from this patch to a deep copy of _data_. + + Arguments: + data: A Python object representing JSON-like data. + + Returns: + A patched, deep copy of _data_. + + Raises: + JSONPatchError: When a patch operation fails. + JSONPatchTestFailure: When a _test_ operation does not pass. + `JSONPatchTestFailure` is a subclass of `JSONPatchError`. + """ + return self.apply(copy.deepcopy(data)) + def asdicts(self) -> List[Dict[str, object]]: """Return a list of this patch's operations as dictionaries.""" return [op.asdict() for op in self.ops] @@ -690,7 +709,7 @@ def apply( If _data_ is a string or file-like object, it will be loaded with _json.loads_. Otherwise _data_ should be a JSON-like data structure and - will be **modified in-place**. + will be **modified in-place**, even if a patch operation fails. When modifying _data_ in-place, we return modified data too. This is to allow for replacing _data's_ root element, which is allowed by some @@ -711,10 +730,41 @@ def apply( JSONPatchError: When a patch operation fails. JSONPatchTestFailure: When a _test_ operation does not pass. `JSONPatchTestFailure` is a subclass of `JSONPatchError`. - """ return JSONPatch( patch, unicode_escape=unicode_escape, uri_decode=uri_decode, ).apply(data) + + +def patch( + patch: Union[str, IOBase, Iterable[Mapping[str, object]], None], + data: Union[MutableSequence[Any], MutableMapping[str, Any]], + *, + unicode_escape: bool = True, + uri_decode: bool = False, +) -> object: + """Apply the JSON Patch _patch_ to a deep copy of _data_. + + Arguments: + patch: A JSON Patch formatted document or equivalent Python objects. + data: A Python object representing JSON-like data. + unicode_escape: If `True`, UTF-16 escape sequences will be decoded + before parsing JSON pointers. + uri_decode: If `True`, JSON pointers will be unescaped using _urllib_ + before being parsed. + + Returns: + A patched, deep copy of _data_. + + Raises: + JSONPatchError: When a patch operation fails. + JSONPatchTestFailure: When a _test_ operation does not pass. + `JSONPatchTestFailure` is a subclass of `JSONPatchError`. + """ + return JSONPatch( + patch, + unicode_escape=unicode_escape, + uri_decode=uri_decode, + ).patch(data) diff --git a/tests/cts b/tests/cts index b9d7153..7be7c1f 160000 --- a/tests/cts +++ b/tests/cts @@ -1 +1 @@ -Subproject commit b9d7153e58711ad38bb8e35ece69c13f4b2f7d63 +Subproject commit 7be7c1fc28057c91e8eefaf197060fba7ed43acd diff --git a/tests/test_json_patch.py b/tests/test_json_patch.py index a362858..3c7d487 100644 --- a/tests/test_json_patch.py +++ b/tests/test_json_patch.py @@ -1,11 +1,15 @@ """JSON Patch test cases.""" +import copy import json import re from collections.abc import Mapping +from contextlib import suppress from io import StringIO from typing import Any +from typing import Dict from typing import Iterator +from typing import List import pytest @@ -273,3 +277,38 @@ def test_non_standard_addap_op() -> None: def test_add_to_mapping_with_int_key() -> None: patch = JSONPatch().add(path="/1", value=99) assert patch.apply({"foo": 1}) == {"foo": 1, "1": 99} + + +def test_apply_does_not_copy_data() -> None: + """Test that _apply_ modifies data in place, even if the patch fails.""" + patch_doc: List[Dict[str, Any]] = [ + {"op": "replace", "path": "/a/b/c", "value": 42}, + {"op": "test", "path": "/a/b/c", "value": "C"}, + ] + + data: Dict[str, Any] = {"a": {"b": {"c": 1}}} + data_ = copy.deepcopy(data) + + patch = JSONPatch(patch_doc) + + with suppress(JSONPatchError): + patch.apply(data) + + assert data != data_ + + +def test_patch_always_copies_data() -> None: + patch_doc: List[Dict[str, Any]] = [ + {"op": "replace", "path": "/a/b/c", "value": 42}, + {"op": "test", "path": "/a/b/c", "value": "C"}, + ] + + data: Dict[str, Any] = {"a": {"b": {"c": 1}}} + data_ = copy.deepcopy(data) + + patcher = JSONPatch(patch_doc) + + with suppress(JSONPatchError): + patcher.patch(data) + + assert data == data_ From 8eed5c0393787c3c267b53102ce2dbdfa3ff186b Mon Sep 17 00:00:00 2001 From: James Prior Date: Thu, 25 Jun 2026 12:39:02 +0100 Subject: [PATCH 2/4] Exclude py 3.8 and 3.9 from CI for latest Hatch and pytest --- .github/workflows/tests.yaml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d43eeb0..75b14ba 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -9,16 +9,8 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] exclude: - - os: macos-latest - python-version: "3.8" - - os: windows-latest - python-version: "3.8" - - os: macos-latest - python-version: "3.9" - - os: windows-latest - python-version: "3.9" - os: macos-latest python-version: "3.10" - os: windows-latest From c6dfab59f2988e44f1727377d929988d11c2f496 Mon Sep 17 00:00:00 2001 From: James Prior Date: Thu, 25 Jun 2026 14:41:37 +0100 Subject: [PATCH 3/4] Rename `patch` to `atomic` and mutate input on success --- CHANGELOG.md | 2 +- README.md | 28 +++++++++++++++++++++-- docs/quickstart.md | 14 +++++++----- jsonpath/patch.py | 34 +++++++++++++++++++--------- pyproject.toml | 6 ++--- tests/test_json_patch.py | 48 ++++++++++++++++++++++++++++++++++++---- 6 files changed, 106 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be37c6..c46e130 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Version 2.1.0 -Added `JSONPatch.patch(data)` that performs a deep copy of `data` before applying the patch. In contrast, `JSONPatch.apply(data)` does not perform a deep copy, always modifying `data` in place, even if a patch operation fails. See [#129](https://github.com/jg-rp/python-jsonpath/issues/129). +Added `patch.atomic(patch, data)` and `JSONPatch.atomic(data)`. `atomic()` is similar to `apply()`, but preserves input data if a patch operation fails. See [#129](https://github.com/jg-rp/python-jsonpath/issues/129). ## Version 2.0.2 diff --git a/README.md b/README.md index 78d4440..35e8c01 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,8 @@ print(jane_score) # 55 We also include an [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902) compliant implementation of JSON Patch. See JSON Patch [quick start](https://jg-rp.github.io/python-jsonpath/quickstart/#patchapplypatch-data) and the [API reference](https://jg-rp.github.io/python-jsonpath/api/#jsonpath.JSONPatch). -> [!NOTE] -> Note that objects passed to `patch.apply()` and `JSONPatch.apply()` are modified in place, even if a patch operation fails. +> [!WARNING] +> Objects passed to `patch.apply()` and `JSONPatch.apply()` are modified in place, even if a patch operation fails. Use `patch.atomic()` or `JSONPatch.atomic()` if you need to preserve input data on patch failure. ```python from jsonpath import patch @@ -139,6 +139,30 @@ patch.apply(patch_operations, data) print(data) # {'some': {'other': 'thing', 'foo': {'bar': [1], 'else': 'thing'}}} ``` +Use `patch.atomic()` or `JSONPatch.atomic()` if you need to preserve input data on patch failure. + +```python +import contextlib + +from jsonpath import JSONPatchError +from jsonpath import patch + +patch_operations = [ + {"op": "add", "path": "/some/foo", "value": {"foo": {}}}, + {"op": "add", "path": "/some/foo", "value": {"bar": []}}, + {"op": "copy", "from": "/some/other", "path": "/some/foo/else"}, + {"op": "add", "path": "/some/foo/bar/-", "value": 1}, + {"op": "test", "path": "/some/thing", "value": "baz"}, # Always fails +] + +data = {"some": {"other": "thing"}} + +with contextlib.suppress(JSONPatchError): + patch.atomic(patch_operations, data) + +assert data == {"some": {"other": "thing"}} +``` + ## License `python-jsonpath` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/docs/quickstart.md b/docs/quickstart.md index b82721d..6090b2f 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -274,7 +274,7 @@ _data_ is the target JSON document to modify. If _data_ is a string or file-like !!! warning - Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `JSONPatch.patch()`, which performs a deep copy for you. + Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `JSONPatch.atomic()`, which performs a deep copy and merge on success. ```python from jsonpath import patch @@ -328,13 +328,15 @@ patch.apply(data) print(data) # {'some': {'other': 'thing', 'foo': {'bar': [1], 'else': 'thing'}}} ``` -## `JSONPatch.patch(data)` +## `patch.atomic(patch, data)` -Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `JSONPatch.patch()`, which performs a deep copy for you. +**_New in version 2.1.0_** + +Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `patch.atomic()`, which performs a deep copy and merge on success. !!! note - `JSONPatch.patch(data)` accepts JSON-like like dictionaries and lists, whereas `JSONPatch.apply(data)` accepts file-like objects, JSON-formatted strings or JSON-like data. + `patch.atomic()` and `JSONPatch.atomic()` are limited to JSON-like like dictionaries and lists, whereas `patch.apply()` and `JSONPatch.apply(data)` accept file-like objects, JSON-formatted strings or JSON-like data. ```python import copy @@ -343,7 +345,7 @@ from typing import Any from jsonpath import JSONPatch from jsonpath import JSONPatchError -patcher = JSONPatch( +patch = JSONPatch( [ {"op": "replace", "path": "/a/b/c", "value": 42}, {"op": "test", "path": "/a/b/c", "value": "C"}, # Always fails @@ -354,7 +356,7 @@ data: dict[str, Any] = {"a": {"b": {"c": 1}}} data_ = copy.deepcopy(data) try: - patcher.patch(data) + patch.atomic(data) except JSONPatchError: # TODO: something pass diff --git a/jsonpath/patch.py b/jsonpath/patch.py index a93820f..f7c5d7b 100644 --- a/jsonpath/patch.py +++ b/jsonpath/patch.py @@ -674,24 +674,36 @@ def apply( return _data - def patch( + def atomic( self, - data: Union[MutableSequence[Any], MutableMapping[str, Any]], + data: Union[List[Any], Dict[str, Any]], ) -> object: - """Apply all operations from this patch to a deep copy of _data_. + """Apply this patch to _data_ atomically. + + Unlike `apply()`, if any patch operation fails, _data_ remains + unchanged. Arguments: data: A Python object representing JSON-like data. Returns: - A patched, deep copy of _data_. + Patched _data_. Raises: JSONPatchError: When a patch operation fails. JSONPatchTestFailure: When a _test_ operation does not pass. `JSONPatchTestFailure` is a subclass of `JSONPatchError`. """ - return self.apply(copy.deepcopy(data)) + data_ = copy.deepcopy(data) + self.apply(data_) # This could raise a JSONPatchError. + data.clear() + + if isinstance(data, dict): + data.update(data_) + else: + data.extend(data_) + + return data def asdicts(self) -> List[Dict[str, object]]: """Return a list of this patch's operations as dictionaries.""" @@ -738,14 +750,16 @@ def apply( ).apply(data) -def patch( +def atomic( patch: Union[str, IOBase, Iterable[Mapping[str, object]], None], - data: Union[MutableSequence[Any], MutableMapping[str, Any]], + data: Union[List[Any], Dict[str, Any]], *, unicode_escape: bool = True, uri_decode: bool = False, ) -> object: - """Apply the JSON Patch _patch_ to a deep copy of _data_. + """Apply patch operations from _patch_ to _data_ atomically. + + Unlike `apply()`, if any patch operation fails, _data_ remains unchanged. Arguments: patch: A JSON Patch formatted document or equivalent Python objects. @@ -756,7 +770,7 @@ def patch( before being parsed. Returns: - A patched, deep copy of _data_. + Patched _data_. Raises: JSONPatchError: When a patch operation fails. @@ -767,4 +781,4 @@ def patch( patch, unicode_escape=unicode_escape, uri_decode=uri_decode, - ).patch(data) + ).atomic(data) diff --git a/pyproject.toml b/pyproject.toml index e5acb63..02182e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,7 +107,7 @@ exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] [tool.mypy] files = ["jsonpath", "tests"] exclude = ["tests/nts", "tests/cts"] -python_version = "3.11" +python_version = "3.10" disallow_subclassing_any = true disallow_untyped_calls = true disallow_untyped_defs = true @@ -152,8 +152,8 @@ exclude = [ line-length = 88 -# Assume Python 3.10. -target-version = "py310" +# Assume Python 3.8. +target-version = "py38" [tool.ruff.lint] select = [ diff --git a/tests/test_json_patch.py b/tests/test_json_patch.py index 3c7d487..7e3660d 100644 --- a/tests/test_json_patch.py +++ b/tests/test_json_patch.py @@ -297,18 +297,58 @@ def test_apply_does_not_copy_data() -> None: assert data != data_ -def test_patch_always_copies_data() -> None: +def test_atomic_patch_success() -> None: patch_doc: List[Dict[str, Any]] = [ {"op": "replace", "path": "/a/b/c", "value": 42}, - {"op": "test", "path": "/a/b/c", "value": "C"}, + {"op": "add", "path": "/a/b/d", "value": 2}, + ] + + data: Dict[str, Any] = {"a": {"b": {"c": 1}}} + patch = JSONPatch(patch_doc) + patch.atomic(data) + assert data == {"a": {"b": {"c": 42, "d": 2}}} + + +def test_atomic_patch_fail() -> None: + patch_doc: List[Dict[str, Any]] = [ + {"op": "replace", "path": "/a/b/c", "value": 42}, + {"op": "test", "path": "/a/b/c", "value": "C"}, # Always fails ] data: Dict[str, Any] = {"a": {"b": {"c": 1}}} data_ = copy.deepcopy(data) - patcher = JSONPatch(patch_doc) + patch = JSONPatch(patch_doc) + + with suppress(JSONPatchError): + patch.atomic(data) + + assert data == data_ + + +def test_atomic_patch_array_success() -> None: + patch_doc: List[Dict[str, Any]] = [ + {"op": "add", "path": "/2", "value": "c"}, + ] + + data = ["a", "b"] + patch = JSONPatch(patch_doc) + patch.atomic(data) + assert data == ["a", "b", "c"] + + +def test_atomic_patch_array_fail() -> None: + patch_doc: List[Dict[str, Any]] = [ + {"op": "add", "path": "/2", "value": "c"}, + {"op": "test", "path": "/2", "value": "x"}, # Always fails + ] + + data = ["a", "b"] + data_ = copy.deepcopy(data) + + patch = JSONPatch(patch_doc) with suppress(JSONPatchError): - patcher.patch(data) + patch.atomic(data) assert data == data_ From 977c3ccd0516623b327c86881fe8f70803ed67eb Mon Sep 17 00:00:00 2001 From: James Prior Date: Thu, 25 Jun 2026 14:47:53 +0100 Subject: [PATCH 4/4] Clarify input data mutation on success --- docs/quickstart.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/quickstart.md b/docs/quickstart.md index 6090b2f..6f4298d 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -274,7 +274,7 @@ _data_ is the target JSON document to modify. If _data_ is a string or file-like !!! warning - Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `JSONPatch.atomic()`, which performs a deep copy and merge on success. + Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `JSONPatch.atomic()`, which performs a deep copy, then mutates input data on success. ```python from jsonpath import patch @@ -332,7 +332,7 @@ print(data) # {'some': {'other': 'thing', 'foo': {'bar': [1], 'else': 'thing'}} **_New in version 2.1.0_** -Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `patch.atomic()`, which performs a deep copy and merge on success. +Data passed to `patch.apply()` and `JSONPatch.apply()` is modified in place, even if a patch operation fails. If you need to preserve input data on failure, you should make a copy of your data before calling `apply()`, or use `patch.atomic()`, which performs a deep copy, then mutates input data on success. !!! note