diff --git a/pydantic/fields.py b/pydantic/fields.py index 01792b6..6ab039c 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -1,6 +1,5 @@ import warnings from collections.abc import Iterable as CollectionsIterable -from copy import deepcopy from typing import ( TYPE_CHECKING, Any, @@ -36,7 +35,7 @@ from .typing import ( is_new_type, new_type_supertype, ) -from .utils import PyObjectStr, Representation, lenient_issubclass, sequence_like +from .utils import PyObjectStr, Representation, lenient_issubclass, sequence_like, smart_deepcopy from .validators import constant_validator, dict_validator, find_validators, validate_json Required: Any = Ellipsis @@ -271,14 +270,7 @@ class ModelField(Representation): self.prepare() def get_default(self) -> Any: - if self.default_factory is not None: - value = self.default_factory() - elif self.default is None: - # deepcopy is quite slow on None - value = None - else: - value = deepcopy(self.default) - return value + return smart_deepcopy(self.default) if self.default_factory is None else self.default_factory() @classmethod def infer( diff --git a/pydantic/main.py b/pydantic/main.py index c872f1e..c6c486a 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -42,6 +42,7 @@ from .utils import ( generate_model_signature, lenient_issubclass, sequence_like, + smart_deepcopy, unique_list, validate_field_name, ) @@ -219,7 +220,7 @@ class ModelMetaclass(ABCMeta): pre_root_validators, post_root_validators = [], [] for base in reversed(bases): if _is_base_model_class_defined and issubclass(base, BaseModel) and base != BaseModel: - fields.update(deepcopy(base.__fields__)) + fields.update(smart_deepcopy(base.__fields__)) config = inherit_config(base.__config__, config) validators = inherit_validators(base.__validators__, validators) pre_root_validators += base.__pre_root_validators__ @@ -527,7 +528,7 @@ class BaseModel(Representation, metaclass=ModelMetaclass): Default values are respected, but no other validation is performed. """ m = cls.__new__(cls) - object.__setattr__(m, '__dict__', {**deepcopy(cls.__field_defaults__), **values}) + object.__setattr__(m, '__dict__', {**smart_deepcopy(cls.__field_defaults__), **values}) if _fields_set is None: _fields_set = set(values.keys()) object.__setattr__(m, '__fields_set__', _fields_set) @@ -558,6 +559,7 @@ class BaseModel(Representation, metaclass=ModelMetaclass): ) if deep: + # chances of having empty dict here are quite low for using smart_deepcopy v = deepcopy(v) cls = self.__class__ diff --git a/pydantic/utils.py b/pydantic/utils.py index d14a285..42a4d30 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -1,6 +1,9 @@ import warnings +import weakref +from collections import OrderedDict, defaultdict, deque +from copy import deepcopy from itertools import islice -from types import GeneratorType +from types import BuiltinFunctionType, CodeType, FunctionType, GeneratorType, LambdaType, ModuleType from typing import ( TYPE_CHECKING, AbstractSet, @@ -20,7 +23,7 @@ from typing import ( no_type_check, ) -from .typing import display_as_type +from .typing import NoneType, display_as_type from .version import version_info if TYPE_CHECKING: @@ -50,6 +53,41 @@ __all__ = ( 'ClassAttribute', ) +# these are types that are returned unchanged by deepcopy +IMMUTABLE_NON_COLLECTIONS_TYPES: Set[Type[Any]] = { + int, + float, + complex, + str, + bool, + bytes, + type, + NoneType, + FunctionType, + BuiltinFunctionType, + LambdaType, + weakref.ref, + CodeType, + # note: including ModuleType will differ from behaviour of deepcopy by not producing error. + # It might be not a good idea in general, but considering that this function used only internally + # against default values of fields, this will allow to actually have a field with module as default value + ModuleType, + NotImplemented.__class__, + Ellipsis.__class__, +} + +# these are types that if empty, might be copied with simple copy() instead of deepcopy() +BUILTIN_COLLECTIONS: Set[Type[Any]] = { + list, + set, + tuple, + frozenset, + dict, + OrderedDict, + defaultdict, + deque, +} + def import_string(dotted_path: str) -> Any: """ @@ -534,3 +572,22 @@ class ClassAttribute: if instance is None: return self.value raise AttributeError(f'{self.name!r} attribute of {owner.__name__!r} is class-only') + + +Obj = TypeVar('Obj') + + +def smart_deepcopy(obj: Obj) -> Obj: + """ + Return type as is for immutable built-in types + Use obj.copy() for built-in empty collections + Use copy.deepcopy() for non-empty collections and unknown objects + """ + + obj_type = obj.__class__ + if obj_type in IMMUTABLE_NON_COLLECTIONS_TYPES: + return obj # fastest case: obj is immutable and not collection therefore will not be copied anyway + elif not obj and obj_type in BUILTIN_COLLECTIONS: + # faster way for empty collections, no need to copy its members + return obj if obj_type is tuple else obj.copy() # type: ignore # tuple doesn't have copy method + return deepcopy(obj) # slowest way when we actually might need a deepcopy diff --git a/tests/test_utils.py b/tests/test_utils.py index d59f2e3..935038f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,7 @@ import os import re import string +from copy import deepcopy from distutils.version import StrictVersion from enum import Enum from typing import NewType, Union @@ -13,12 +14,14 @@ from pydantic.dataclasses import dataclass from pydantic.fields import Undefined from pydantic.typing import Literal, all_literal_values, display_as_type, is_new_type, new_type_supertype from pydantic.utils import ( + BUILTIN_COLLECTIONS, ClassAttribute, ValueItems, deep_update, get_model, import_string, lenient_issubclass, + smart_deepcopy, truncate, unique_list, ) @@ -347,3 +350,30 @@ def test_all_literal_values(): L312 = Literal['3', Literal[L1, L2]] assert sorted(all_literal_values(L312)) == sorted(('1', '2', '3')) + + +@pytest.mark.parametrize( + 'obj', + (1, 1.0, '1', b'1', int, None, test_all_literal_values, len, test_all_literal_values.__code__, lambda: ..., ...), +) +def test_smart_deepcopy_immutable_non_sequence(obj, mocker): + # make sure deepcopy is not used + # (other option will be to use obj.copy(), but this will produce error as none of given objects have this method) + mocker.patch('pydantic.utils.deepcopy', side_effect=RuntimeError) + assert smart_deepcopy(obj) is deepcopy(obj) is obj + + +@pytest.mark.parametrize('empty_collection', (collection() for collection in BUILTIN_COLLECTIONS)) +def test_smart_deepcopy_empty_collection(empty_collection, mocker): + mocker.patch('pydantic.utils.deepcopy', side_effect=RuntimeError) # make sure deepcopy is not used + if not isinstance(empty_collection, (tuple, frozenset)): # empty tuple or frozenset are always the same object + assert smart_deepcopy(empty_collection) is not empty_collection + + +@pytest.mark.parametrize( + 'collection', (c.fromkeys((1,)) if issubclass(c, dict) else c((1,)) for c in BUILTIN_COLLECTIONS) +) +def test_smart_deepcopy_collection(collection, mocker): + expected_value = object() + mocker.patch('pydantic.utils.deepcopy', return_value=expected_value) + assert smart_deepcopy(collection) is expected_value