diff --git a/changes/3777-PrettyWood.md b/changes/3777-PrettyWood.md new file mode 100644 index 0000000..97475e0 --- /dev/null +++ b/changes/3777-PrettyWood.md @@ -0,0 +1 @@ +support overwriting dunder attributes of `BaseModel` instances diff --git a/pydantic/main.py b/pydantic/main.py index 4b8daec..8c0e9f8 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -53,6 +53,7 @@ from .typing import ( update_model_forward_refs, ) from .utils import ( + DUNDER_ATTRIBUTES, ROOT_KEY, ClassAttribute, GetterDict, @@ -350,7 +351,7 @@ class BaseModel(Representation, metaclass=ModelMetaclass): @no_type_check def __setattr__(self, name, value): # noqa: C901 (ignore complexity) - if name in self.__private_attributes__: + if name in self.__private_attributes__ or name in DUNDER_ATTRIBUTES: return object_setattr(self, name, value) if self.__config__.extra is not Extra.allow and name not in self.__fields__: @@ -891,7 +892,9 @@ class BaseModel(Representation, metaclass=ModelMetaclass): def __repr_args__(self) -> 'ReprArgs': return [ - (k, v) for k, v in self.__dict__.items() if k not in self.__fields__ or self.__fields__[k].field_info.repr + (k, v) + for k, v in self.__dict__.items() + if k not in DUNDER_ATTRIBUTES and (k not in self.__fields__ or self.__fields__[k].field_info.repr) ] diff --git a/pydantic/utils.py b/pydantic/utils.py index ffec554..51c0253 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -78,6 +78,7 @@ __all__ = ( 'ROOT_KEY', 'get_unique_discriminator_alias', 'get_discriminator_alias_and_values', + 'DUNDER_ATTRIBUTES', 'LimitedDict', ) @@ -691,15 +692,19 @@ def is_valid_field(name: str) -> bool: return ROOT_KEY == name +DUNDER_ATTRIBUTES = { + '__annotations__', + '__classcell__', + '__doc__', + '__module__', + '__orig_bases__', + '__orig_class__', + '__qualname__', +} + + def is_valid_private_name(name: str) -> bool: - return not is_valid_field(name) and name not in { - '__annotations__', - '__classcell__', - '__doc__', - '__module__', - '__orig_bases__', - '__qualname__', - } + return not is_valid_field(name) and name not in DUNDER_ATTRIBUTES _EMPTY = object() diff --git a/tests/test_main.py b/tests/test_main.py index ceac222..16faaba 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -19,6 +19,7 @@ from typing import ( from uuid import UUID, uuid4 import pytest +from typing_extensions import Annotated from pydantic import ( BaseConfig, @@ -2174,6 +2175,19 @@ def test_new_union_origin(): } +def test_annotated_class(): + class PydanticModel(BaseModel): + foo: str = '123' + + PydanticAlias = Annotated[PydanticModel, 'bar baz'] + + pa = PydanticAlias() + assert isinstance(pa, PydanticModel) + pa.__doc__ = 'qwe' + assert repr(pa) == "PydanticModel(foo='123')" + assert pa.__doc__ == 'qwe' + + @pytest.mark.parametrize( 'ann', [Final, Final[int]],