python-jsonpath 2.0.2__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/PKG-INFO +28 -2
  2. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/README.md +27 -1
  3. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/__about__.py +1 -1
  4. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/env.py +4 -4
  5. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/patch.py +70 -6
  6. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/pyproject.toml +3 -3
  7. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/.gitignore +0 -0
  8. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/LICENSE.txt +0 -0
  9. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/__init__.py +0 -0
  10. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/__main__.py +0 -0
  11. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/_data.py +0 -0
  12. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/_types.py +0 -0
  13. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/cli.py +0 -0
  14. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/exceptions.py +0 -0
  15. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/filter.py +0 -0
  16. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/fluent_api.py +0 -0
  17. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/__init__.py +0 -0
  18. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/_pattern.py +0 -0
  19. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/arguments.py +0 -0
  20. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/count.py +0 -0
  21. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/filter_function.py +0 -0
  22. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/is_instance.py +0 -0
  23. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/keys.py +0 -0
  24. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/length.py +0 -0
  25. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/match.py +0 -0
  26. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/search.py +0 -0
  27. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/starts_with.py +0 -0
  28. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/typeof.py +0 -0
  29. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/function_extensions/value.py +0 -0
  30. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/lex.py +0 -0
  31. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/lru_cache.py +0 -0
  32. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/match.py +0 -0
  33. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/parse.py +0 -0
  34. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/path.py +0 -0
  35. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/pointer.py +0 -0
  36. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/py.typed +0 -0
  37. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/segments.py +0 -0
  38. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/selectors.py +0 -0
  39. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/serialize.py +0 -0
  40. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/stream.py +0 -0
  41. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/token.py +0 -0
  42. {python_jsonpath-2.0.2 → python_jsonpath-2.1.0}/jsonpath/unescape.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-jsonpath
3
- Version: 2.0.2
3
+ Version: 2.1.0
4
4
  Summary: JSONPath, JSON Pointer and JSON Patch for Python.
5
5
  Project-URL: Documentation, https://jg-rp.github.io/python-jsonpath/
6
6
  Project-URL: Issues, https://github.com/jg-rp/python-jsonpath/issues
@@ -149,7 +149,10 @@ print(jane_score) # 55
149
149
 
150
150
  ### JSON Patch
151
151
 
152
- 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)
152
+ 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).
153
+
154
+ > [!WARNING]
155
+ > 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.
153
156
 
154
157
  ```python
155
158
  from jsonpath import patch
@@ -164,7 +167,30 @@ patch_operations = [
164
167
  data = {"some": {"other": "thing"}}
165
168
  patch.apply(patch_operations, data)
166
169
  print(data) # {'some': {'other': 'thing', 'foo': {'bar': [1], 'else': 'thing'}}}
170
+ ```
171
+
172
+ Use `patch.atomic()` or `JSONPatch.atomic()` if you need to preserve input data on patch failure.
173
+
174
+ ```python
175
+ import contextlib
176
+
177
+ from jsonpath import JSONPatchError
178
+ from jsonpath import patch
179
+
180
+ patch_operations = [
181
+ {"op": "add", "path": "/some/foo", "value": {"foo": {}}},
182
+ {"op": "add", "path": "/some/foo", "value": {"bar": []}},
183
+ {"op": "copy", "from": "/some/other", "path": "/some/foo/else"},
184
+ {"op": "add", "path": "/some/foo/bar/-", "value": 1},
185
+ {"op": "test", "path": "/some/thing", "value": "baz"}, # Always fails
186
+ ]
187
+
188
+ data = {"some": {"other": "thing"}}
189
+
190
+ with contextlib.suppress(JSONPatchError):
191
+ patch.atomic(patch_operations, data)
167
192
 
193
+ assert data == {"some": {"other": "thing"}}
168
194
  ```
169
195
 
170
196
  ## License
@@ -119,7 +119,10 @@ print(jane_score) # 55
119
119
 
120
120
  ### JSON Patch
121
121
 
122
- 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)
122
+ 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).
123
+
124
+ > [!WARNING]
125
+ > 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.
123
126
 
124
127
  ```python
125
128
  from jsonpath import patch
@@ -134,7 +137,30 @@ patch_operations = [
134
137
  data = {"some": {"other": "thing"}}
135
138
  patch.apply(patch_operations, data)
136
139
  print(data) # {'some': {'other': 'thing', 'foo': {'bar': [1], 'else': 'thing'}}}
140
+ ```
141
+
142
+ Use `patch.atomic()` or `JSONPatch.atomic()` if you need to preserve input data on patch failure.
143
+
144
+ ```python
145
+ import contextlib
146
+
147
+ from jsonpath import JSONPatchError
148
+ from jsonpath import patch
149
+
150
+ patch_operations = [
151
+ {"op": "add", "path": "/some/foo", "value": {"foo": {}}},
152
+ {"op": "add", "path": "/some/foo", "value": {"bar": []}},
153
+ {"op": "copy", "from": "/some/other", "path": "/some/foo/else"},
154
+ {"op": "add", "path": "/some/foo/bar/-", "value": 1},
155
+ {"op": "test", "path": "/some/thing", "value": "baz"}, # Always fails
156
+ ]
157
+
158
+ data = {"some": {"other": "thing"}}
159
+
160
+ with contextlib.suppress(JSONPatchError):
161
+ patch.atomic(patch_operations, data)
137
162
 
163
+ assert data == {"some": {"other": "thing"}}
138
164
  ```
139
165
 
140
166
  ## License
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2023-present James Prior <jamesgr.prior@gmail.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "2.0.2"
4
+ __version__ = "2.1.0"
@@ -3,18 +3,18 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  try:
6
- import regex # noqa: F401
6
+ import regex # type: ignore # noqa: F401
7
7
 
8
8
  REGEX_AVAILABLE = True
9
9
  except ImportError:
10
- REGEX_AVAILABLE = False
10
+ REGEX_AVAILABLE = False # type: ignore
11
11
 
12
12
  try:
13
- import iregexp_check # noqa: F401
13
+ import iregexp_check # type: ignore # noqa: F401
14
14
 
15
15
  IREGEXP_AVAILABLE = True
16
16
  except ImportError:
17
- IREGEXP_AVAILABLE = False
17
+ IREGEXP_AVAILABLE = False # type: ignore
18
18
 
19
19
  from decimal import Decimal
20
20
  from operator import getitem
@@ -71,7 +71,7 @@ class OpAdd(Op):
71
71
  if target == "-":
72
72
  parent.append(self.value)
73
73
  else:
74
- index = self.path._index(target) # noqa: SLF001
74
+ index = self.path._index(target) # type: ignore # noqa: SLF001
75
75
  if index == len(parent):
76
76
  parent.append(self.value)
77
77
  else:
@@ -303,12 +303,12 @@ class OpCopy(Op):
303
303
  self, data: Union[MutableSequence[object], MutableMapping[str, object]]
304
304
  ) -> Union[MutableSequence[object], MutableMapping[str, object]]:
305
305
  """Apply this patch operation to _data_."""
306
- source_parent, source_obj = self.source.resolve_parent(data)
306
+ _, source_obj = self.source.resolve_parent(data)
307
307
 
308
308
  if source_obj is UNDEFINED:
309
309
  raise JSONPatchError("source object does not exist")
310
310
 
311
- dest_parent, dest_obj = self.dest.resolve_parent(data)
311
+ dest_parent, _ = self.dest.resolve_parent(data)
312
312
 
313
313
  if dest_parent is None:
314
314
  # Copy source to root
@@ -639,7 +639,7 @@ class JSONPatch:
639
639
 
640
640
  If _data_ is a string or file-like object, it will be loaded with
641
641
  _json.loads_. Otherwise _data_ should be a JSON-like data structure and
642
- will be modified in place.
642
+ will be modified in place, even if a patch operation fails.
643
643
 
644
644
  When modifying _data_ in place, we return modified data too. This is
645
645
  to allow for replacing _data's_ root element, which is allowed by some
@@ -674,6 +674,37 @@ class JSONPatch:
674
674
 
675
675
  return _data
676
676
 
677
+ def atomic(
678
+ self,
679
+ data: Union[List[Any], Dict[str, Any]],
680
+ ) -> object:
681
+ """Apply this patch to _data_ atomically.
682
+
683
+ Unlike `apply()`, if any patch operation fails, _data_ remains
684
+ unchanged.
685
+
686
+ Arguments:
687
+ data: A Python object representing JSON-like data.
688
+
689
+ Returns:
690
+ Patched _data_.
691
+
692
+ Raises:
693
+ JSONPatchError: When a patch operation fails.
694
+ JSONPatchTestFailure: When a _test_ operation does not pass.
695
+ `JSONPatchTestFailure` is a subclass of `JSONPatchError`.
696
+ """
697
+ data_ = copy.deepcopy(data)
698
+ self.apply(data_) # This could raise a JSONPatchError.
699
+ data.clear()
700
+
701
+ if isinstance(data, dict):
702
+ data.update(data_)
703
+ else:
704
+ data.extend(data_)
705
+
706
+ return data
707
+
677
708
  def asdicts(self) -> List[Dict[str, object]]:
678
709
  """Return a list of this patch's operations as dictionaries."""
679
710
  return [op.asdict() for op in self.ops]
@@ -690,7 +721,7 @@ def apply(
690
721
 
691
722
  If _data_ is a string or file-like object, it will be loaded with
692
723
  _json.loads_. Otherwise _data_ should be a JSON-like data structure and
693
- will be **modified in-place**.
724
+ will be **modified in-place**, even if a patch operation fails.
694
725
 
695
726
  When modifying _data_ in-place, we return modified data too. This is
696
727
  to allow for replacing _data's_ root element, which is allowed by some
@@ -711,10 +742,43 @@ def apply(
711
742
  JSONPatchError: When a patch operation fails.
712
743
  JSONPatchTestFailure: When a _test_ operation does not pass.
713
744
  `JSONPatchTestFailure` is a subclass of `JSONPatchError`.
714
-
715
745
  """
716
746
  return JSONPatch(
717
747
  patch,
718
748
  unicode_escape=unicode_escape,
719
749
  uri_decode=uri_decode,
720
750
  ).apply(data)
751
+
752
+
753
+ def atomic(
754
+ patch: Union[str, IOBase, Iterable[Mapping[str, object]], None],
755
+ data: Union[List[Any], Dict[str, Any]],
756
+ *,
757
+ unicode_escape: bool = True,
758
+ uri_decode: bool = False,
759
+ ) -> object:
760
+ """Apply patch operations from _patch_ to _data_ atomically.
761
+
762
+ Unlike `apply()`, if any patch operation fails, _data_ remains unchanged.
763
+
764
+ Arguments:
765
+ patch: A JSON Patch formatted document or equivalent Python objects.
766
+ data: A Python object representing JSON-like data.
767
+ unicode_escape: If `True`, UTF-16 escape sequences will be decoded
768
+ before parsing JSON pointers.
769
+ uri_decode: If `True`, JSON pointers will be unescaped using _urllib_
770
+ before being parsed.
771
+
772
+ Returns:
773
+ Patched _data_.
774
+
775
+ Raises:
776
+ JSONPatchError: When a patch operation fails.
777
+ JSONPatchTestFailure: When a _test_ operation does not pass.
778
+ `JSONPatchTestFailure` is a subclass of `JSONPatchError`.
779
+ """
780
+ return JSONPatch(
781
+ patch,
782
+ unicode_escape=unicode_escape,
783
+ uri_decode=uri_decode,
784
+ ).atomic(data)
@@ -107,7 +107,7 @@ exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"]
107
107
  [tool.mypy]
108
108
  files = ["jsonpath", "tests"]
109
109
  exclude = ["tests/nts", "tests/cts"]
110
- python_version = "3.11"
110
+ python_version = "3.10"
111
111
  disallow_subclassing_any = true
112
112
  disallow_untyped_calls = true
113
113
  disallow_untyped_defs = true
@@ -152,8 +152,8 @@ exclude = [
152
152
  line-length = 88
153
153
 
154
154
 
155
- # Assume Python 3.10.
156
- target-version = "py310"
155
+ # Assume Python 3.8.
156
+ target-version = "py38"
157
157
 
158
158
  [tool.ruff.lint]
159
159
  select = [