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

allow for shallow copies #4093

Merged
merged 13 commits into from Aug 10, 2022
2 changes: 2 additions & 0 deletions changes/4093-timkpaine.md
@@ -0,0 +1,2 @@
Allow for shallow copies of attributes, adjusting the behavior of #3642
`Config.copy_on_model_validation` is now a str enum of `["", "deep", "shallow"]` corresponding to reference, deep copy, shallow copy.
11 changes: 8 additions & 3 deletions pydantic/config.py
@@ -1,6 +1,6 @@
import json
from enum import Enum
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, Literal, Optional, Tuple, Type, Union

from .typing import AnyCallable
from .utils import GetterDict
Expand Down Expand Up @@ -36,6 +36,9 @@ class Extra(str, Enum):
forbid = 'forbid'


Copy = Literal["none", "deep", "shallow"]


class BaseConfig:
title: Optional[str] = None
anystr_lower: bool = False
Expand Down Expand Up @@ -63,8 +66,10 @@ class BaseConfig:
json_encoders: Dict[Union[Type[Any], str], AnyCallable] = {}
underscore_attrs_are_private: bool = False

# whether inherited models as fields should be reconstructed as base model
copy_on_model_validation: bool = True
# whether inherited models as fields should be reconstructed as base model,
# and whether such a copy should be shallow or deep
copy_on_model_validation: Copy = "deep"

# whether `Union` should check all allowed types before even trying to coerce
smart_union: bool = False

Expand Down
13 changes: 11 additions & 2 deletions pydantic/main.py
Expand Up @@ -25,7 +25,7 @@
)

from .class_validators import ValidatorGroup, extract_root_validators, extract_validators, inherit_validators
from .config import BaseConfig, Extra, inherit_config, prepare_config
from .config import BaseConfig, Copy, Extra, inherit_config, prepare_config
from .error_wrappers import ErrorWrapper, ValidationError
from .errors import ConfigError, DictError, ExtraError, MissingError
from .fields import MAPPING_LIKE_SHAPES, Field, FieldInfo, ModelField, ModelPrivateAttr, PrivateAttr, Undefined
Expand Down Expand Up @@ -675,9 +675,18 @@ def __get_validators__(cls) -> 'CallableGenerator':
@classmethod
def validate(cls: Type['Model'], value: Any) -> 'Model':
if isinstance(value, cls):
if cls.__config__.copy_on_model_validation:
if isinstance(cls.__config__.copy_on_model_validation, bool):
# Warn about deprecated behavior
warnings.warn('`copy_on_model_validation` should be a string of value ("deep", "shallow", "none")', RuntimeWarning)

if cls.__config__.copy_on_model_validation == "shallow":
samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved
# shallow copy
return value._copy_and_set_values(value.__dict__, value.__fields_set__, deep=False)
elif cls.__config__.copy_on_model_validation == "deep":
# deep copy
return value._copy_and_set_values(value.__dict__, value.__fields_set__, deep=True)
else:
# ref
return value

value = cls._enforce_dict_if_root(value)
Expand Down
22 changes: 22 additions & 0 deletions tests/test_main.py
Expand Up @@ -1567,6 +1567,28 @@ class Config:
assert t.dict() == {'id': '1234567890', 'user': {'id': 42, 'hobbies': ['scuba diving']}}


def test_model_exclude_copy_on_model_validation_shallow():
"""When `Config.copy_on_model_validation` is set and `Config.copy_on_model_validation_shallow` is set,
do the same as the previous test but perform a shallow copy"""

class User(BaseModel):
class Config:
copy_on_model_validation = 'shallow'

hobbies: List[str]

my_user = User(hobbies=['scuba diving'])

class Transaction(BaseModel):
user: User = Field(...)

t = Transaction(
user=my_user,
)

assert t.user.hobbies is my_user.hobbies # unlike above, this should be a shallow copy


def test_validation_deep_copy():
"""By default, Config.copy_on_model_validation should do a deep copy"""

Expand Down