Skip to content

Commit

Permalink
Correct docs, logic for model_construct behavior with extra (#8807)
Browse files Browse the repository at this point in the history
  • Loading branch information
sydney-runkle committed Feb 14, 2024
1 parent b290b31 commit e99bf8a
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 9 deletions.
10 changes: 8 additions & 2 deletions docs/concepts/models.md
Expand Up @@ -617,13 +617,19 @@ Here are some additional notes on the behavior of [`model_construct()`][pydantic
[`model_construct()`][pydantic.main.BaseModel.model_construct].
* In particular, the [`model_construct()`][pydantic.main.BaseModel.model_construct] method does not support recursively constructing models from dicts.
* If you do not pass keyword arguments for fields with defaults, the default values will still be used.
* For models with `model_config['extra'] == 'allow'`, data not corresponding to fields will be correctly stored in
the `__pydantic_extra__` dict.
* For models with private attributes, the `__pydantic_private__` dict will be initialized the same as it would be when
calling `__init__`.
* When constructing an instance using [`model_construct()`][pydantic.main.BaseModel.model_construct], no `__init__` method from the model or any of its parent
classes will be called, even when a custom `__init__` method is defined.

!!! note "On `extra` behavior with `model_construct`"
* For models with `model_config['extra'] == 'allow'`, data not corresponding to fields will be correctly stored in
the `__pydantic_extra__` dict and saved to the model's `__dict__`.
* For models with `model_config['extra'] == 'ignore'`, data not corresponding to fields will be ignored - that is,
not stored in `__pydantic_extra__` or `__dict__` on the instance.
* Unlike a call to `__init__`, a call to `model_construct` with `model_config['extra'] == 'forbid'` doesn't raise an
error in the presence of data not corresponding to fields. Rather, said input data is simply ignored.

## Generic models

Pydantic supports the creation of generic models to make it easier to reuse a common model structure.
Expand Down
10 changes: 7 additions & 3 deletions pydantic/main.py
Expand Up @@ -202,7 +202,13 @@ def model_construct(cls: type[Model], _fields_set: set[str] | None = None, **val
Creates a new model setting `__dict__` and `__pydantic_fields_set__` from trusted or pre-validated data.
Default values are respected, but no other validation is performed.
Behaves as if `Config.extra = 'allow'` was set since it adds all passed values
!!! note
`model_construct()` generally respects the `model_config.extra` setting on the provided model.
That is, if `model_config.extra == 'allow'`, then all extra passed values are added to the model instance's `__dict__`
and `__pydantic_extra__` fields. If `model_config.extra == 'ignore'` (the default), then all extra passed values are ignored.
Because no validation is performed with a call to `model_construct()`, having `model_config.extra == 'forbid'` does not result in
an error if extra values are passed, but they will be ignored.
Args:
_fields_set: The set of field names accepted for the Model instance.
Expand Down Expand Up @@ -232,8 +238,6 @@ def model_construct(cls: type[Model], _fields_set: set[str] | None = None, **val
_extra = {}
for k, v in values.items():
_extra[k] = v
else:
fields_values.update(values)
_object_setattr(m, '__dict__', fields_values)
_object_setattr(m, '__pydantic_fields_set__', _fields_set)
if not cls.__pydantic_root_model__:
Expand Down
20 changes: 16 additions & 4 deletions tests/test_construction.py
Expand Up @@ -37,18 +37,30 @@ def test_construct_fields_set():
assert m.model_dump() == {'a': 3, 'b': -1}


@pytest.mark.parametrize('extra', ['allow', 'ignore', 'forbid'])
def test_construct_allow_extra(extra: str):
"""model_construct() should allow extra fields regardless of the config"""
def test_construct_allow_extra():
"""model_construct() should allow extra fields only in the case of extra='allow'"""

class Foo(BaseModel, extra=extra):
class Foo(BaseModel, extra='allow'):
x: int

model = Foo.model_construct(x=1, y=2)
assert model.x == 1
assert model.y == 2


@pytest.mark.parametrize('extra', ['ignore', 'forbid'])
def test_construct_ignore_extra(extra: str) -> None:
"""model_construct() should ignore extra fields only in the case of extra='ignore' or extra='forbid'"""

class Foo(BaseModel, extra=extra):
x: int

model = Foo.model_construct(x=1, y=2)
assert model.x == 1
assert model.__pydantic_extra__ is None
assert 'y' not in model.__dict__


def test_construct_keep_order():
class Foo(BaseModel):
a: int
Expand Down

0 comments on commit e99bf8a

Please sign in to comment.