8000 modify behaviour of the construct method (#898) · ag-python/pydantic@677677e · GitHub
[go: up one dir, main page]

Skip to content

Commit 677677e

Browse files
authored
modify behaviour of the construct method (pydantic#898)
* modify behaviour of the construct method * change construct signature * Add example for construct function (pydantic#907) * add example for construct * edit exporting_models * typo * add changes file * code review changes * fix bad copy paste * extend example in docs * use __field_defaults__ in construct
1 parent cb262da commit 677677e

File tree

7 files changed

+85
-15
lines changed

7 files changed

+85
-15
lines changed

changes/898-samuelcolvin.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Change the signature of `Model.construct()` to be more user-friendly, document `construct()` usage.

changes/907-ashears.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add example for the `construct()` method

docs/examples/ex_construct.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pydantic import BaseModel
2+
3+
class User(BaseModel):
4+
id: int
5+
age: int
6+
name: str = 'John Doe'
7+
8+
original_user = User(id=123, age=32)
9+
10+
user_data = original_user.dict()
11+
print(user_data)
12+
fields_set = original_user.__fields_set__
13+
print(fields_set)
14+
15+
# ...
16+
# pass user_data and fields_set to RPC or save to the database etc.
17+
# ...
18+
19+
# you can then create a new instance of User without
20+
# re-running validation which would be unnecessary at this point:
21+
new_user = User.construct(_fields_set=fields_set, **user_data)
22+
print(repr(new_user))
23+
print(new_user.__fields_set__)
24+
25+
# construct can be dangerous, only use it with validated data!:
26+
bad_user = User.construct(id='dog')
27+
print(repr(bad_user))

docs/usage/models.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ Models possess the following methods and attributes:
9292
`schema_json()`
9393
: returns a JSON string representation of `schema()`; cf. [Schema](schema.md)
9494

95+
`construct()`
96+
: a class method for creating models without running validation;
97+
cf. [Creating models without validation](#creating-models-without-validation)
98+
9599
`__fields_set__`
96100
: Set of names of fields which were set when the model instance was initialised
97101

@@ -233,6 +237,28 @@ _(This script is complete, it should run "as is")_
233237
!!! info
234238
Because it can result in arbitrary code execution, as a security measure, you need
235239
to explicitly pass `allow_pickle` to the parsing function in order to load `pickle` data.
240+
241+
### Creating models without validation
242+
243+
*pydantic* also provides the `construct()` method which allows models to be created **without validation** this
244+
can be useful when data has already been validated or comes from a trusted source and you want to create a model
245+
as efficiently as possible (`construct()` is generally around 30x faster than creating a model with full validation).
246+
247+
!!! warning
248+
`construct()` does not do any validation, meaning it can create models which are invalid. **You should only
249+
ever use the `construct()` method with data which has already been validated, or you trust.**
250+
251+
```py
252+
{!.tmp_examples/ex_construct.py!}
253+
```
254+
_(This script is complete, it should run "as is")_
255+
256+
The `_fields_set` keyword argument to `construct()` is optional, but allows you to be more precise about
257+
which fields were originally set and which weren't. If it's omitted `__fields_set__` will just be the keys
258+
of the data provided.
259+
260+
For example, in the example above, if `_fields_set` was not provided,
261+
`new_user.__fields_set__` would be `{'id', 'age', 'name'}`.
236262

237263
## Generic Models
238264

pydantic/main.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -439,14 +439,16 @@ def from_orm(cls: Type['Model'], obj: Any) -> 'Model':
439439
return m
440440

441441
@classmethod
442-
def construct(cls: Type['Model'], values: 'DictAny', fields_set: 'SetStr') -> 'Model':
442+
def construct(cls: Type['Model'], _fields_set: Optional['SetStr'] = None, **values: Any) -> 'Model':
443443
"""
444-
Creates a new model and set __dict__ without any validation, thus values should already be trusted.
445-
Chances are you don't want to use this method directly.
444+
Creates a new model setting __dict__ and __fields_set__ from trusted or pre-validated data.
445+
Default values are respected, but no other validation is performed.
446446
"""
447447
m = cls.__new__(cls)
448-
object.__setattr__(m, '__dict__', values)
449-
object.__setattr__(m, '__fields_set__', fields_set)
448+
object.__setattr__(m, '__dict__', {**deepcopy(cls.__field_defaults__), **values})
449+
if _fields_set is None:
450+
_fields_set = set(values.keys())
451+
object.__setattr__(m, '__fields_set__', _fields_set)
450452
return m
451453

452454
def copy(
@@ -491,7 +493,11 @@ def copy(
491493

492494
if deep:
493495
v = deepcopy(v)
494-
m = self.__class__.construct(v, self.__fields_set__.copy())
496+
497+
cls = self.__class__
498+
m = cls.__new__(cls)
499+
object.__setattr__(m, '__dict__', v)
500+
object.__setattr__(m, '__fields_set__', self.__fields_set__.copy())
495501
return m
496502

497503
@classmethod

tests/test_construction.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,27 @@ class Model(BaseModel):
1212

1313

1414
def test_simple_construct():
15-
m = Model.construct(dict(a=40, b=10), {'a', 'b'})
16-
assert m.a == 40
15+
m = Model.construct(a=3.14)
16+
assert m.a == 3.14
1717
assert m.b == 10
18+
assert m.__fields_set__ == {'a'}
19+
assert m.dict() == {'a': 3.14, 'b': 10}
1820

1921

20-
def test_construct_missing():
21-
m = Model.construct(dict(a='not a float'), {'a'})
22-
assert m.a == 'not a float'
23-
with pytest.raises(AttributeError) as exc_info:
24-
print(m.b)
22+
def test_construct_misuse():
23+
m = Model.construct(b='foobar')
24+
assert m.b == 'foobar'
25+
assert m.dict() == {'b': 'foobar'}
26+
with pytest.raises(AttributeError, match="'Model' object has no attribute 'a'"):
27+
print(m.a)
2528

26-
assert "'Model' object has no attribute 'b'" in exc_info.value.args[0]
29+
30+
def test_construct_fields_set():
31+
m = Model.construct(a=3.0, b=-1, _fields_set={'a'})
32+
assert m.a == 3
33+
assert m.b == -1
34+
assert m.__fields_set__ == {'a'}
35+
assert m.dict() == {'a': 3, 'b': -1}
2736

2837

2938
def test_large_any_str():

tests/test_edge_cases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -554,7 +554,7 @@ class Model(BaseModel):
554554

555555
assert Model(a=1, field_set=2).dict() == {'a': 1, 'field_set': 2, 'b': 3}
556556
assert Model(a=1, field_set=2).dict(exclude_unset=True) == {'a': 1, 'field_set': 2}
557-
assert Model.construct(dict(a=1, field_set=3), {'a', 'field_set'}).dict() == {'a': 1, 'field_set': 3}
557+
assert Model.construct(a=1, field_set=3).dict() == {'a': 1, 'field_set': 3, 'b': 3}
558558

559559

560560
def test_values_order():

0 commit comments

Comments
 (0)
0