pydantic-json-patch 0.6.0__tar.gz → 0.6.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pydantic_json_patch
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Pydantic models for implementing JSON Patch.
5
5
  Author: Jonathan Sharpe
6
6
  Author-email: Jonathan Sharpe <mail@jonrshar.pe>
@@ -81,7 +81,7 @@ ReplaceOp[str](op='replace', path='/foo/bar', value='hello')
81
81
 
82
82
  Additionally, there are two compound types:
83
83
 
84
- - `Operation` is the union of all the operators; and
84
+ - `Operation` is the union of all the operations; and
85
85
  - `JsonPatch` is a Pydantic `RootModel` representing a sequence of operations.
86
86
 
87
87
  `JsonPatch` can be used directly for validation:
@@ -152,6 +152,32 @@ and list the models along with the other schemas:
152
152
 
153
153
  [![Screenshot of Swagger UI schema list][swagger-schemas]][swagger-schemas]
154
154
 
155
+ ### Value type validation
156
+
157
+ You can also use a more specific type to apply type validation to the value properties:
158
+
159
+ ```python
160
+ import typing as tp
161
+ from uuid import UUID
162
+
163
+ from fastapi import Body, FastAPI
164
+ from pydantic import Discriminator
165
+
166
+ from pydantic_json_patch import AddOp, TestOp
167
+
168
+ app = FastAPI()
169
+
170
+
171
+ @app.patch("/resource/{resource_id}")
172
+ def _(
173
+ resource_id: UUID,
174
+ operations: tp.Annotated[list[tp.Annotated[AddOp[int] | TestOp[int], Discriminator("op")]], Body()],
175
+ ) -> ...:
176
+ ...
177
+ ```
178
+
179
+ **Note**: explicitly specifying the [discriminator][pydantic-discriminator] gives better results on _failed_ validation for unions of operations.
180
+
155
181
  ## Development
156
182
 
157
183
  This project uses [uv] for managing dependencies.
@@ -198,6 +224,7 @@ This will auto-restart as you make changes.
198
224
  [json patch]: https://datatracker.ietf.org/doc/html/rfc6902/
199
225
  [json pointer]: https://datatracker.ietf.org/doc/html/rfc6901/
200
226
  [pydantic]: https://docs.pydantic.dev/latest/
227
+ [pydantic-discriminator]: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions-with-str-discriminators
201
228
  [pypi]: https://pypi.org/
202
229
  [pypi-badge]: https://img.shields.io/pypi/v/pydantic-json-patch?logo=python&logoColor=white&label=PyPI
203
230
  [pypi-page]: https://pypi.org/project/pydantic-json-patch/
@@ -49,7 +49,7 @@ ReplaceOp[str](op='replace', path='/foo/bar', value='hello')
49
49
 
50
50
  Additionally, there are two compound types:
51
51
 
52
- - `Operation` is the union of all the operators; and
52
+ - `Operation` is the union of all the operations; and
53
53
  - `JsonPatch` is a Pydantic `RootModel` representing a sequence of operations.
54
54
 
55
55
  `JsonPatch` can be used directly for validation:
@@ -120,6 +120,32 @@ and list the models along with the other schemas:
120
120
 
121
121
  [![Screenshot of Swagger UI schema list][swagger-schemas]][swagger-schemas]
122
122
 
123
+ ### Value type validation
124
+
125
+ You can also use a more specific type to apply type validation to the value properties:
126
+
127
+ ```python
128
+ import typing as tp
129
+ from uuid import UUID
130
+
131
+ from fastapi import Body, FastAPI
132
+ from pydantic import Discriminator
133
+
134
+ from pydantic_json_patch import AddOp, TestOp
135
+
136
+ app = FastAPI()
137
+
138
+
139
+ @app.patch("/resource/{resource_id}")
140
+ def _(
141
+ resource_id: UUID,
142
+ operations: tp.Annotated[list[tp.Annotated[AddOp[int] | TestOp[int], Discriminator("op")]], Body()],
143
+ ) -> ...:
144
+ ...
145
+ ```
146
+
147
+ **Note**: explicitly specifying the [discriminator][pydantic-discriminator] gives better results on _failed_ validation for unions of operations.
148
+
123
149
  ## Development
124
150
 
125
151
  This project uses [uv] for managing dependencies.
@@ -166,6 +192,7 @@ This will auto-restart as you make changes.
166
192
  [json patch]: https://datatracker.ietf.org/doc/html/rfc6902/
167
193
  [json pointer]: https://datatracker.ietf.org/doc/html/rfc6901/
168
194
  [pydantic]: https://docs.pydantic.dev/latest/
195
+ [pydantic-discriminator]: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions-with-str-discriminators
169
196
  [pypi]: https://pypi.org/
170
197
  [pypi-badge]: https://img.shields.io/pypi/v/pydantic-json-patch?logo=python&logoColor=white&label=PyPI
171
198
  [pypi-page]: https://pypi.org/project/pydantic-json-patch/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pydantic_json_patch"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "Pydantic models for implementing JSON Patch."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -72,3 +72,25 @@ profile = "black"
72
72
  [tool.pytest.ini_options]
73
73
  addopts = "--doctest-glob=*.md" # validate README examples
74
74
  python_classes = [] # avoid trying to run TestOp as a test
75
+
76
+ [tool.ruff.lint]
77
+ select = ["ALL"]
78
+ ignore = [
79
+ "COM812",
80
+ "D203",
81
+ "D213",
82
+ "D105",
83
+ ]
84
+
85
+ [tool.ruff.lint.per-file-ignores]
86
+ "tests/**/*.py" = [
87
+ "ANN201",
88
+ "ANN401",
89
+ "C408",
90
+ "D",
91
+ "PLR2004",
92
+ "S101",
93
+ ]
94
+ "bin/**/*.py" = [
95
+ "D",
96
+ ]
@@ -1,3 +1,9 @@
1
+ """Pydantic models for implementing `JSON Patch`_.
2
+
3
+ .. _JSON Patch: https://datatracker.ietf.org/doc/html/rfc6902/
4
+
5
+ """
6
+
1
7
  from importlib.metadata import version
2
8
 
3
9
  from .models import (
@@ -14,7 +20,6 @@ from .models import (
14
20
  __version__ = version(__name__)
15
21
 
16
22
  __all__ = [
17
- "__version__",
18
23
  "AddOp",
19
24
  "CopyOp",
20
25
  "JsonPatch",
@@ -23,4 +28,5 @@ __all__ = [
23
28
  "RemoveOp",
24
29
  "ReplaceOp",
25
30
  "TestOp",
31
+ "__version__",
26
32
  ]
@@ -1,6 +1,8 @@
1
+ """Core models and private implementation."""
2
+
1
3
  import re
2
4
  import typing as tp
3
- from collections.abc import Sequence
5
+ from collections.abc import Iterator, Sequence
4
6
  from functools import cached_property, lru_cache
5
7
 
6
8
  import typing_extensions as tx
@@ -21,14 +23,21 @@ T = tx.TypeVar("T", default=tp.Any)
21
23
  # region base models
22
24
 
23
25
 
26
+ def _generate_title(model: type[tp.Any]) -> str:
27
+ """Strip any type metadata and expand the shortened name."""
28
+ name, *_ = model.__name__.partition("[")
29
+ return f"JsonPatch{name}eration"
30
+
31
+
24
32
  class _BaseOp(BaseModel):
25
33
  model_config = ConfigDict(
26
34
  frozen=True,
27
- model_title_generator=lambda t: f"JsonPatch{t.__name__}eration",
35
+ model_title_generator=_generate_title,
28
36
  )
29
37
 
30
38
  @classmethod
31
- def create(cls, *, path: str | Sequence[str], **kwargs) -> tx.Self:
39
+ def create(cls, *, path: str | Sequence[str], **kwargs: tp.Any) -> tx.Self: # noqa: ANN401
40
+ """Return an instance of the appropriate operation."""
32
41
  (op,) = tp.get_args(cls.model_fields["op"].annotation)
33
42
  return cls(op=op, path=cls._dump_pointer(path), **kwargs)
34
43
 
@@ -68,8 +77,12 @@ class _BaseOp(BaseModel):
68
77
  class _FromOp(_BaseOp):
69
78
  @classmethod
70
79
  def create(
71
- cls, *, path: str | Sequence[str], from_: str | Sequence[str]
80
+ cls,
81
+ *,
82
+ path: str | Sequence[str],
83
+ from_: str | Sequence[str],
72
84
  ) -> tx.Self: # ty: ignore[invalid-method-override] -- deliberately narrows **kwargs to named params
85
+ """Return an instance of the appropriate operation."""
73
86
  pointer = from_ if isinstance(from_, str) else cls._dump_pointer(from_)
74
87
  return super().create(path=path, **{"from": pointer})
75
88
 
@@ -78,7 +91,7 @@ class _FromOp(_BaseOp):
78
91
 
79
92
  @model_validator(mode="before")
80
93
  @classmethod
81
- def _pre_validate(cls, data: tp.Any, info: ValidationInfo) -> tp.Any:
94
+ def _pre_validate(cls, data: tp.Any, info: ValidationInfo) -> tp.Any: # noqa: ANN401
82
95
  if (
83
96
  info.mode != "json" # pragma: no mutate
84
97
  and isinstance(data, dict)
@@ -97,6 +110,7 @@ class _FromOp(_BaseOp):
97
110
  class _ValueOp(_BaseOp, tp.Generic[T]):
98
111
  @classmethod
99
112
  def create(cls, *, path: str | Sequence[str], value: T) -> tx.Self: # ty: ignore[invalid-method-override] -- deliberately narrows **kwargs to named params
113
+ """Return an instance of the appropriate operation."""
100
114
  return super().create(path=path, value=value)
101
115
 
102
116
  value: T = Field(examples=[42])
@@ -109,30 +123,67 @@ class _ValueOp(_BaseOp, tp.Generic[T]):
109
123
 
110
124
 
111
125
  class AddOp(_ValueOp[T], tp.Generic[T]):
126
+ """Represents the `add`_ operation.
127
+
128
+ .. _add: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.1
129
+
130
+ """
131
+
112
132
  op: tp.Literal["add"]
113
133
 
114
134
 
115
135
  class CopyOp(_FromOp):
136
+ """Represents the `copy`_ operation.
137
+
138
+ .. _copy: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.5
139
+
140
+ """
141
+
116
142
  op: tp.Literal["copy"]
117
143
 
118
144
 
119
145
  class MoveOp(_FromOp):
146
+ """Represents the `move`_ operation.
147
+
148
+ .. _move: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.4
149
+
150
+ """
151
+
120
152
  op: tp.Literal["move"]
121
153
 
122
154
 
123
155
  class RemoveOp(_BaseOp):
156
+ """Represents the `remove`_ operation.
157
+
158
+ .. _remove: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.2
159
+
160
+ """
161
+
124
162
  @classmethod
125
163
  def create(cls, *, path: str | Sequence[str]) -> tx.Self: # ty: ignore[invalid-method-override] -- deliberately narrows **kwargs to named params
164
+ """Return an instance of the appropriate operation."""
126
165
  return super().create(path=path)
127
166
 
128
167
  op: tp.Literal["remove"]
129
168
 
130
169
 
131
170
  class ReplaceOp(_ValueOp[T], tp.Generic[T]):
171
+ """Represents the `replace`_ operation.
172
+
173
+ .. _replace: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.3
174
+
175
+ """
176
+
132
177
  op: tp.Literal["replace"]
133
178
 
134
179
 
135
180
  class TestOp(_ValueOp[T], tp.Generic[T]):
181
+ """Represents the `test`_ operation.
182
+
183
+ .. _test: https://datatracker.ietf.org/doc/html/rfc6902/#section-4.6
184
+
185
+ """
186
+
136
187
  op: tp.Literal["test"]
137
188
 
138
189
 
@@ -141,16 +192,23 @@ class TestOp(_ValueOp[T], tp.Generic[T]):
141
192
  # region compound models
142
193
 
143
194
  Operation: tp.TypeAlias = tp.Annotated[
144
- tp.Union[AddOp, CopyOp, MoveOp, RemoveOp, ReplaceOp, TestOp], Discriminator("op")
195
+ AddOp | CopyOp | MoveOp | RemoveOp | ReplaceOp | TestOp,
196
+ Discriminator("op"),
145
197
  ]
146
198
 
147
199
 
148
200
  class JsonPatch(RootModel[Sequence[Operation]], Sequence[Operation]):
201
+ """Represents a full JSON Patch `document`_.
202
+
203
+ .. _document: https://datatracker.ietf.org/doc/html/rfc6902/#section-3
204
+
205
+ """
206
+
149
207
  model_config = ConfigDict(frozen=True)
150
208
 
151
209
  @model_validator(mode="before")
152
210
  @classmethod
153
- def _coerce_to_tuple(cls, value: tp.Any) -> tuple[tp.Any, ...]:
211
+ def _coerce_to_tuple(cls, value: tp.Any) -> tuple[tp.Any, ...]: # noqa: ANN401
154
212
  if isinstance(value, Sequence) and not isinstance(value, tuple):
155
213
  return tuple(value)
156
214
  return value
@@ -164,7 +222,7 @@ class JsonPatch(RootModel[Sequence[Operation]], Sequence[Operation]):
164
222
  def __getitem__(self, index):
165
223
  return self.root[index]
166
224
 
167
- def __iter__(self):
225
+ def __iter__(self) -> Iterator[Operation]: # ty: ignore[invalid-method-override] -- dict(model) doesn't make sense for a sequence of non-pairs
168
226
  return iter(self.root)
169
227
 
170
228
  def __len__(self) -> int: