Skip to content

Commit

Permalink
Raise an error when deleting frozen (model) fields (#7800)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexmojaki committed Oct 12, 2023
1 parent b2e7ee3 commit 4348153
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 18 deletions.
18 changes: 16 additions & 2 deletions docs/errors/validation_errors.md
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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`
Expand Down
36 changes: 21 additions & 15 deletions pydantic/main.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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]
Expand All @@ -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:
Expand Down
19 changes: 18 additions & 1 deletion tests/test_main.py
Expand Up @@ -515,27 +515,44 @@ 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
assert exc_info.value.errors(include_url=False) == [
{'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):
Expand Down

0 comments on commit 4348153

Please sign in to comment.