mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
fix: models copied via Config.copy_on_model_validation always have all fields (#3201)
Small regression in #2231. The shallow copy done with `Config.copy_on_model_validation = True` (default behaviour) was using excluded / included fields when it should just copy everything closes #3195
This commit is contained in:
+24
-17
@@ -586,6 +586,24 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
|
||||
m._init_private_attributes()
|
||||
return m
|
||||
|
||||
def _copy_and_set_values(self: 'Model', values: 'DictStrAny', fields_set: 'SetStr', *, deep: bool) -> 'Model':
|
||||
if deep:
|
||||
# chances of having empty dict here are quite low for using smart_deepcopy
|
||||
values = deepcopy(values)
|
||||
|
||||
cls = self.__class__
|
||||
m = cls.__new__(cls)
|
||||
object_setattr(m, '__dict__', values)
|
||||
object_setattr(m, '__fields_set__', fields_set)
|
||||
for name in self.__private_attributes__:
|
||||
value = getattr(self, name, Undefined)
|
||||
if value is not Undefined:
|
||||
if deep:
|
||||
value = deepcopy(value)
|
||||
object_setattr(m, name, value)
|
||||
|
||||
return m
|
||||
|
||||
def copy(
|
||||
self: 'Model',
|
||||
*,
|
||||
@@ -605,32 +623,18 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
|
||||
:return: new model instance
|
||||
"""
|
||||
|
||||
v = dict(
|
||||
values = dict(
|
||||
self._iter(to_dict=False, by_alias=False, include=include, exclude=exclude, exclude_unset=False),
|
||||
**(update or {}),
|
||||
)
|
||||
|
||||
if deep:
|
||||
# chances of having empty dict here are quite low for using smart_deepcopy
|
||||
v = deepcopy(v)
|
||||
|
||||
cls = self.__class__
|
||||
m = cls.__new__(cls)
|
||||
object_setattr(m, '__dict__', v)
|
||||
# new `__fields_set__` can have unset optional fields with a set value in `update` kwarg
|
||||
if update:
|
||||
fields_set = self.__fields_set__ | update.keys()
|
||||
else:
|
||||
fields_set = set(self.__fields_set__)
|
||||
object_setattr(m, '__fields_set__', fields_set)
|
||||
for name in self.__private_attributes__:
|
||||
value = getattr(self, name, Undefined)
|
||||
if value is not Undefined:
|
||||
if deep:
|
||||
value = deepcopy(value)
|
||||
object_setattr(m, name, value)
|
||||
|
||||
return m
|
||||
return self._copy_and_set_values(values, fields_set, deep=deep)
|
||||
|
||||
@classmethod
|
||||
def schema(cls, by_alias: bool = True, ref_template: str = default_ref_template) -> 'DictStrAny':
|
||||
@@ -658,7 +662,10 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
|
||||
@classmethod
|
||||
def validate(cls: Type['Model'], value: Any) -> 'Model':
|
||||
if isinstance(value, cls):
|
||||
return value.copy() if cls.__config__.copy_on_model_validation else value
|
||||
if cls.__config__.copy_on_model_validation:
|
||||
return value._copy_and_set_values(value.__dict__, value.__fields_set__, deep=False)
|
||||
else:
|
||||
return value
|
||||
|
||||
value = cls._enforce_dict_if_root(value)
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ from pydantic import (
|
||||
Field,
|
||||
NoneBytes,
|
||||
NoneStr,
|
||||
PrivateAttr,
|
||||
Required,
|
||||
SecretStr,
|
||||
ValidationError,
|
||||
constr,
|
||||
root_validator,
|
||||
@@ -1516,6 +1518,45 @@ def test_model_exclude_config_field_merging():
|
||||
assert Model.__fields__['b'].field_info.exclude == {'foo': ..., 'bar': ...}
|
||||
|
||||
|
||||
def test_model_exclude_copy_on_model_validation():
|
||||
"""When `Config.copy_on_model_validation` is set, it should keep private attributes and excluded fields"""
|
||||
|
||||
class User(BaseModel):
|
||||
_priv: int = PrivateAttr()
|
||||
id: int
|
||||
username: str
|
||||
password: SecretStr = Field(exclude=True)
|
||||
hobbies: List[str]
|
||||
|
||||
my_user = User(id=42, username='JohnDoe', password='hashedpassword', hobbies=['scuba diving'])
|
||||
|
||||
my_user._priv = 13
|
||||
assert my_user.id == 42
|
||||
assert my_user.password.get_secret_value() == 'hashedpassword'
|
||||
assert my_user.dict() == {'id': 42, 'username': 'JohnDoe', 'hobbies': ['scuba diving']}
|
||||
|
||||
class Transaction(BaseModel):
|
||||
id: str
|
||||
user: User = Field(..., exclude={'username'})
|
||||
value: int
|
||||
|
||||
class Config:
|
||||
fields = {'value': {'exclude': True}}
|
||||
|
||||
t = Transaction(
|
||||
id='1234567890',
|
||||
user=my_user,
|
||||
value=9876543210,
|
||||
)
|
||||
|
||||
assert t.user is not my_user
|
||||
assert t.user.hobbies == ['scuba diving']
|
||||
assert t.user.hobbies is my_user.hobbies # `Config.copy_on_model_validation` only does a shallow copy
|
||||
assert t.user._priv == 13
|
||||
assert t.user.password.get_secret_value() == 'hashedpassword'
|
||||
assert t.dict() == {'id': '1234567890', 'user': {'id': 42, 'hobbies': ['scuba diving']}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'kinds',
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user