Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct docs, logic for model_construct behavior with extra #8807

Merged
merged 3 commits into from Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved

## 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