diff --git a/docs/errors/validation_errors.md b/docs/errors/validation_errors.md index 615c616fbf..b857c45f59 100644 --- a/docs/errors/validation_errors.md +++ b/docs/errors/validation_errors.md @@ -715,7 +715,7 @@ except ValidationError as exc: ## `frozen_field` -This error is raised when you attempt to assign a value to a field with `frozen=True`: +This error is raised when you attempt to assign a value to a field with `frozen=True`, or to delete such a field: ```py from pydantic import BaseModel, Field, ValidationError @@ -726,16 +726,23 @@ class Model(BaseModel): model = Model() + try: model.x = 'test1' except ValidationError as exc: print(repr(exc.errors()[0]['type'])) #> 'frozen_field' + +try: + del model.x +except ValidationError as exc: + print(repr(exc.errors()[0]['type'])) + #> 'frozen_field' ``` ## `frozen_instance` -This error is raised when `model_config['frozen] == True` and you attempt to assign a new value to +This error is raised when `model_config['frozen] == True` and you attempt to delete or assign a new value to any of the fields: ```py @@ -749,11 +756,18 @@ class Model(BaseModel): m = Model(x=1) + try: m.x = 2 except ValidationError as exc: print(repr(exc.errors()[0]['type'])) #> 'frozen_instance' + +try: + del m.x +except ValidationError as exc: + print(repr(exc.errors()[0]['type'])) + #> 'frozen_instance' ``` ## `frozen_set_type` diff --git a/pydantic/main.py b/pydantic/main.py index 8ce04e9d1e..89f82d1e9d 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -774,20 +774,8 @@ def __setattr__(self, name: str, value: Any) -> None: else: self.__pydantic_private__[name] = value return - elif self.model_config.get('frozen', None): - error: pydantic_core.InitErrorDetails = { - 'type': 'frozen_instance', - 'loc': (name,), - 'input': value, - } - raise pydantic_core.ValidationError.from_exception_data(self.__class__.__name__, [error]) - elif getattr(self.model_fields.get(name), 'frozen', False): - error: pydantic_core.InitErrorDetails = { - 'type': 'frozen_field', - 'loc': (name,), - 'input': value, - } - raise pydantic_core.ValidationError.from_exception_data(self.__class__.__name__, [error]) + + self._check_frozen(name, value) attr = getattr(self.__class__, name, None) if isinstance(attr, property): @@ -823,9 +811,13 @@ def __delattr__(self, item: str) -> Any: try: # Note: self.__pydantic_private__ cannot be None if self.__private_attributes__ has items del self.__pydantic_private__[item] # type: ignore + return except KeyError as exc: raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') from exc - elif item in self.model_fields: + + self._check_frozen(item, None) + + if item in self.model_fields: object.__delattr__(self, item) elif self.__pydantic_extra__ is not None and item in self.__pydantic_extra__: del self.__pydantic_extra__[item] @@ -835,6 +827,20 @@ def __delattr__(self, item: str) -> Any: except AttributeError: raise AttributeError(f'{type(self).__name__!r} object has no attribute {item!r}') + def _check_frozen(self, name: str, value: Any) -> None: + if self.model_config.get('frozen', None): + typ = 'frozen_instance' + elif getattr(self.model_fields.get(name), 'frozen', False): + typ = 'frozen_field' + else: + return + error: pydantic_core.InitErrorDetails = { + 'type': typ, + 'loc': (name,), + 'input': value, + } + raise pydantic_core.ValidationError.from_exception_data(self.__class__.__name__, [error]) + def __getstate__(self) -> dict[Any, Any]: private = self.__pydantic_private__ if private: diff --git a/tests/test_main.py b/tests/test_main.py index bcd00fc51b..82367b41c2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -515,20 +515,29 @@ class FrozenModel(BaseModel): a: int = 10 m = FrozenModel() - assert m.a == 10 + with pytest.raises(ValidationError) as exc_info: m.a = 11 assert exc_info.value.errors(include_url=False) == [ {'type': 'frozen_instance', 'loc': ('a',), 'msg': 'Instance is frozen', 'input': 11} ] + with pytest.raises(ValidationError) as exc_info: + del m.a + assert exc_info.value.errors(include_url=False) == [ + {'type': 'frozen_instance', 'loc': ('a',), 'msg': 'Instance is frozen', 'input': None} + ] + + assert m.a == 10 + def test_frozen_field(): class FrozenModel(BaseModel): a: int = Field(10, frozen=True) m = FrozenModel() + assert m.a == 10 with pytest.raises(ValidationError) as exc_info: m.a = 11 @@ -536,6 +545,14 @@ class FrozenModel(BaseModel): {'type': 'frozen_field', 'loc': ('a',), 'msg': 'Field is frozen', 'input': 11} ] + with pytest.raises(ValidationError) as exc_info: + del m.a + assert exc_info.value.errors(include_url=False) == [ + {'type': 'frozen_field', 'loc': ('a',), 'msg': 'Field is frozen', 'input': None} + ] + + assert m.a == 10 + def test_not_frozen_are_not_hashable(): class TestModel(BaseModel):