Skip to content

Commit 291cd68

Browse files
authored
Merge pull request #29 from joshorr/josho/better-mypy-support-and-auto-off-by-default
Support mypy + make `PartialModel` not auto-define partial fields
2 parents 59ca3ff + b6b669e commit 291cd68

File tree

11 files changed

+956
-697
lines changed

11 files changed

+956
-697
lines changed

README.md

Lines changed: 96 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ An easy way to add or create partials for Pydantic models.
2121

2222
[//]: # (--8<-- [start:readme])
2323

24+
## Important Upgrade from v1.x Notes
25+
26+
I decided to make the default behavior of `PartialModel` not be automatic anymore.
27+
28+
I made a new class named `AutoPartialModel` that works exactly the same as the old v1.x `PartialModel` previously did.
29+
30+
To upgrade, simply replace `PartialModel` with `AutoPartialModel`, and things will work exactly as they did before.
31+
The `auto_partials` configuration option is still present and if present will still override the base-class setting.
32+
2433
## Quick Start
2534

2635
### Install
@@ -41,21 +50,78 @@ You can create from scratch, or convert existing models to be Partials.
4150
The main purpose will be to add to exiting models, and hence the default
4251
behavior of making all non-default fields partials (configurable).
4352

53+
### Two Partial Base Class Options
54+
55+
There are two options to inherit from:
56+
57+
- `PartialModel`
58+
- With this one, you must explicitly set which fields are partial
59+
- To get correct static type checking, you also can also set a partial field's default value to `Missing`.
60+
- `AutoPartialModel`
61+
- This automatically applies partial behavior to every attribute that does not already have a default value.
62+
63+
4464
Let's first look at a basic example.
4565

46-
### Basic Example
66+
### Explicitly Defined Partials - Basic Example
4767

48-
Very basic example of a simple model follows:
68+
Very basic example of a simple model with explicitly defined partial fields, follows:
4969

5070
```python
51-
from pydantic_partials import PartialModel, Missing
52-
71+
from pydantic_partials import PartialModel, Missing, Partial, MissingType
72+
from pydantic import ValidationError
5373

5474
class MyModel(PartialModel):
75+
some_field: str
76+
partial_field: Partial[str] = Missing
77+
78+
# Alternate Syntax:
79+
alternate_syntax_partial_field: str | MissingType = Missing
80+
81+
82+
# By default, `Partial` fields without any value will get set to a
83+
# special `Missing` type. Any field that is set to Missing is
84+
# excluded from the model_dump/model_dump_json.
85+
obj = MyModel(some_field='a-value')
86+
assert obj.partial_field is Missing
87+
assert obj.model_dump() == {'some_field': 'a-value'}
88+
89+
# You can set the real value at any time, and it will behave like expected.
90+
obj.partial_field = 'hello'
91+
assert obj.partial_field == 'hello'
92+
assert obj.model_dump() == {'some_field': 'a-value', 'partial_field': 'hello'}
93+
94+
# You can always manually set a field to `Missing` directly.
95+
obj.partial_field = Missing
96+
97+
# And now it's removed from the model-dump.
98+
assert obj.model_dump() == {'some_field': 'a-value'}
99+
100+
# The json dump is also affected in the same way.
101+
assert obj.model_dump_json() == '{"some_field":"a-value"}'
102+
103+
try:
104+
# This should produce an error because
105+
# `some_field` is a required field.
106+
MyModel()
107+
except ValidationError as e:
108+
print(f'Pydantic will state `some_field` + `value` are required: {e}')
109+
else:
110+
raise Exception('Pydantic should have required `some_field`.')
111+
```
112+
113+
### Automatically Defined Partials - Basic Example
114+
115+
Very basic example of a simple model with automatically defined partial fields, follows:
116+
117+
```python
118+
from pydantic_partials import AutoPartialModel, Missing
119+
120+
class MyModel(AutoPartialModel):
55121
some_attr: str
56122
another_field: str
57123

58-
# By default, Partial fields without any value will get set to a
124+
# By default, automatic defined partial fields without any value will get set to a
59125
# special `Missing` type. Any field that is set to Missing is
60126
# excluded from the model_dump/model_dump_json.
61127
obj = MyModel()
@@ -91,10 +157,10 @@ This includes any inherited Pydantic fields (from a superclass).
91157

92158
### Inheritable
93159

94-
You can inherit from a model to make a partial-version of the inherited fields:
160+
With `AutoPartialModel`, you can inherit from a model to make an automatic partial-version of the inherited fields:
95161

96162
```python
97-
from pydantic_partials import PartialModel, Missing
163+
from pydantic_partials import AutoPartialModel, Missing
98164
from pydantic import ValidationError, BaseModel
99165

100166
class TestModel(BaseModel):
@@ -109,11 +175,11 @@ try:
109175
except ValidationError as e:
110176
print(f'Pydantic will state `name` + `value` are required: {e}')
111177
else:
112-
raise Exception('Field `required_decimal` should be required.')
178+
raise Exception('Pydantic should have required `required_decimal`.')
113179

114180
# We inherit from `TestModel` and add `PartialModel` to the mix.
115181

116-
class PartialTestModel(PartialModel, TestModel):
182+
class PartialTestModel(AutoPartialModel, TestModel):
117183
pass
118184

119185
# `PartialTestModel` can now be allocated without the required fields.
@@ -137,7 +203,7 @@ Notice that if a field has a default value, it's used instead of marking it as `
137203
Also, the `Missing` sentinel value is a separate value vs `None`, allowing one to easily
138204
know if a value is truly just missing or is `None`/`Null`.
139205

140-
### Exclude Fields From Auto Partials
206+
### Exclude Fields from Automatic Partials (AutoPartialModel)
141207

142208
You can exclude specific fields from the automatic partials via these means:
143209

@@ -155,12 +221,12 @@ You can override an excluded value by explicitly marking a field as Partial via
155221
Here is an example using the `AutoPartialExclude` method, also showing how it can inherit.
156222

157223
```python
158-
from pydantic_partials import PartialModel, AutoPartialExclude, Missing
224+
from pydantic_partials import AutoPartialModel, AutoPartialExclude, Missing
159225
from pydantic import BaseModel, ValidationError
160226
from datetime import datetime
161227
import pytest
162228

163-
class PartialRequired(PartialModel):
229+
class PartialRequired(AutoPartialModel):
164230
id: AutoPartialExclude[str]
165231
created_at: AutoPartialExclude[datetime]
166232

@@ -181,10 +247,11 @@ with pytest.raises(
181247
r'id[\w\W]*Field required[\w\W]*'
182248
r'created_at[\w\W]*Field required'
183249
):
184-
PartialTestModel()
250+
# This should raise a 'ValidationError'
251+
PartialTestModel() # type: ignore
185252

186253
# If we give them values, we get no ValidationError
187-
obj = PartialTestModel(id='some-value', created_at=datetime.now())
254+
obj = PartialTestModel(id='some-value', created_at=datetime.now()) # type: ignore
188255

189256
# And fields have the expected values.
190257
assert obj.id == 'some-value'
@@ -193,16 +260,21 @@ assert obj.name is Missing
193260

194261
### Auto Partials Configuration
195262

196-
You can turn off automatically applying partials to all non-defaulted fields
197-
via `auto_partials` class argument or modeL_config option:
263+
Normally you would simply inherit from either `PartialModel` or `AutoPartialModel`, depending on the desired behavior you want.
264+
265+
But you can also configure the auto-partials aspect via class paramters or the `model_config` attribute:
198266

199267
```python
200-
from pydantic_partials import PartialModel, PartialConfigDict
268+
from pydantic_partials import PartialModel, PartialConfigDict, AutoPartialModel
201269

202-
class TestModel1(PartialModel, auto_partials=False):
270+
# `PartialModel` uses `auto_partials` as `False` by default, but we can override that if you want via class argument:
271+
class TestModel1(PartialModel, auto_partials=True):
203272
...
204273

205-
class TestModel2(PartialModel):
274+
# Or via `model_config`
275+
# (PartialConfigDict inherits from Pydantic's `ConfigDict`,
276+
# so you have all of Pydantic's options still available).
277+
class TestModel2(AutoPartialModel):
206278
model_config = PartialConfigDict(auto_partials=False)
207279
...
208280
```
@@ -211,29 +283,27 @@ You can disable this automatic function. This means you have complete control of
211283
can be partial or not. You can use either the generic `Partial[...]` generic or a union with `MissingType`
212284
to mark a field as a partial field. The generic simple makes the union to MissingType for you.
213285

214-
Example of disabling auto_partials:
215-
216286
```python
217287
from pydantic_partials import PartialModel, Missing, MissingType, Partial
218288
from decimal import Decimal
219289
from pydantic import ValidationError
220290

221-
class TestModel(PartialModel, auto_partials=False):
291+
class TestModel(PartialModel):
222292
# Can use `Partial` generic type
223293
partial_int: Partial[int] = Missing
224-
294+
225295
# Or union with `MissingType`
226296
partial_str: str | MissingType
227-
297+
228298
required_decimal: Decimal
229-
299+
230300
try:
231301
TestModel()
232302
except ValidationError as e:
233303
print(f'Pydantic will state `required_decimal` is required: {e}')
234304
else:
235305
raise Exception('Pydantic should have required `required_decimal`.')
236-
306+
237307
obj = TestModel(required_decimal='1.34')
238308

239309
# You can find out at any time if a field is missing or not:

0 commit comments

Comments
 (0)