-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Description
Checks
- I added a descriptive title to this issue
- I have searched (google, github) for similar issues and couldn't find anything
- I have read and followed the docs and still think this is a bug
Disclaimer
While I understand that the docs state that validators should only raise exceptions of ValueError, TypeError, or AssertionError, it's not unreasonable to assume that other Exception types could be raised by accident/ignorance. Since Pydantic is acting on these unexpected exceptions in a destructive way, I still felt the need to report a bug.
Bug
This is a regression that was introduced with Pydantic 3.7
If a field validator raises an exception that isn't of type ValueError, TypeError, or AssertionError, while Config.validate_assignment is set to True, the field that was being validated is removed from model.__dict__ and can no longer be read or assigned.
Problematic LoC
As far as I can tell, the destructive self.__dict__.pop(name) operation could be replaced with self.__dict__[name].
This way even if an unhandled exception occurs afterwards, the field will still exist on __dict__ and will still have its original value set.
Reproduction of the issue:
from pydantic import validator, BaseModel
class Model(BaseModel):
foo: str = None
class Config:
validate_assignment = True
@validator('foo')
def validate_fields(cls, value):
if value == 'raise_exception':
raise Exception('Example validator error')
return value
if __name__ == '__main__':
model = Model(foo='field1_value')
print('Model before raising exception in validator')
print(f' model={model}')
print(f' model.__fields__={model.__fields__}')
print(f' model.__dict__={model.__dict__}')
try:
model.foo = 'raise_exception'
except Exception:
print('Caught exception as expected')
print('Model after raising exception in validator')
print(f' model={model}')
print(f' model.__fields__={model.__fields__}')
print(f' model.__dict__={model.__dict__}')
# Raises KeyError: 'foo'
# File "pydantic/main.py", line 387, in pydantic.main.BaseModel.__setattr__
model.foo = 'field1_value'Output:
Model before raising exception in validator
model=foo='field1_value'
model.__fields__={'foo': ModelField(name='foo', type=Optional[str], required=False, default=None)}
model.__dict__={'foo': 'field1_value'}
Caught exception as expected
Model after raising exception in validator
model=
model.__fields__={'foo': ModelField(name='foo', type=Optional[str], required=False, default=None)}
model.__dict__={}
Traceback (most recent call last):
File "/Users/john.sabath/random/pydantic-issue/main.py", line 45, in <module>
model.foo = 'field1_value'
File "/Users/john.sabath/venv/pydantic-issue/lib/python3.7/site-packages/pydantic/main.py", line 387, in __setattr__
original_value = self.__dict__.pop(name)
KeyError: 'foo'