mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
Support assert statements inside validators (#653)
* Support assert statements inside validators * Add a validator example that uses assert * Add warning about consequences of using -O optimization flag * Fix a typo * Fix incomplete validator * Extend exception name generation * Improve tests * Clarify pytest behaviour * handle assertion error name, fix build * Address feedback * docs cleanup * Incorporate feedback * fix quotes
This commit is contained in:
committed by
Samuel Colvin
parent
321cde0c88
commit
f41d5dca3c
@@ -35,6 +35,7 @@ mypy:
|
||||
.PHONY: test
|
||||
test:
|
||||
pytest --cov=pydantic
|
||||
@python tests/try_assert.py
|
||||
|
||||
.PHONY: external-mypy
|
||||
external-mypy:
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
add support for ``assert`` statements inside validators
|
||||
@@ -3,6 +3,7 @@ from pydantic import BaseModel, ValidationError, validator
|
||||
|
||||
class UserModel(BaseModel):
|
||||
name: str
|
||||
username: str
|
||||
password1: str
|
||||
password2: str
|
||||
|
||||
@@ -18,6 +19,11 @@ class UserModel(BaseModel):
|
||||
raise ValueError('passwords do not match')
|
||||
return v
|
||||
|
||||
@validator('username')
|
||||
def username_alphanumeric(cls, v):
|
||||
assert v.isalpha(), 'must be alphanumeric'
|
||||
return v
|
||||
|
||||
|
||||
print(UserModel(name='samuel colvin', password1='zxcvbn', password2='zxcvbn'))
|
||||
# > UserModel name='Samuel Colvin' password1='zxcvbn' password2='zxcvbn'
|
||||
|
||||
+9
-1
@@ -194,7 +194,15 @@ A few things to note on validators:
|
||||
* their signature can be ``(cls, value)`` or ``(cls, value, values, config, field)``. As of **v0.20**, any subset of
|
||||
``values``, ``config`` and ``field`` is also permitted, eg. ``(cls, value, field)``, however due to the way
|
||||
validators are inspected, the variadic key word argument ("``**kwargs``") **must** be called ``kwargs``.
|
||||
* validator should either return the new value or raise a ``ValueError`` or ``TypeError``
|
||||
* validators should either return the new value or raise a ``ValueError``, ``TypeError``, or ``AssertionError``
|
||||
(``assert`` statements may be used).
|
||||
|
||||
.. warning::
|
||||
|
||||
If you make use of ``assert`` statements, keep in mind that running
|
||||
Python with the ``-O`` `optimization flag <https://docs.python.org/3/using/cmdline.html#cmdoption-o>`_
|
||||
disables ``assert`` statements, and **validators will stop working**.
|
||||
|
||||
* where validators rely on other values, you should be aware that:
|
||||
|
||||
- Validation is done in the order fields are defined, eg. here ``password2`` has access to ``password1``
|
||||
|
||||
@@ -112,6 +112,8 @@ def flatten_errors(
|
||||
|
||||
@lru_cache()
|
||||
def get_exc_type(cls: Type[Exception]) -> str:
|
||||
if issubclass(cls, AssertionError):
|
||||
return 'assertion_error'
|
||||
|
||||
base_name = 'type_error' if issubclass(cls, TypeError) else 'value_error'
|
||||
if cls in (TypeError, ValueError):
|
||||
|
||||
+1
-1
@@ -448,7 +448,7 @@ class Field:
|
||||
for validator in validators:
|
||||
try:
|
||||
v = validator(cls, v, values, self, self.model_config)
|
||||
except (ValueError, TypeError) as exc:
|
||||
except (ValueError, TypeError, AssertionError) as exc:
|
||||
return v, ErrorWrapper(exc, loc=loc, config=self.model_config)
|
||||
return v, None
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ def test_errors_unknown_error_object():
|
||||
(
|
||||
(TypeError(), 'type_error'),
|
||||
(ValueError(), 'value_error'),
|
||||
(AssertionError(), 'assertion_error'),
|
||||
(errors.DecimalIsNotFiniteError(), 'value_error.decimal.not_finite'),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -637,3 +637,22 @@ def test_make_generic_validator_self():
|
||||
with pytest.raises(ConfigError) as exc_info:
|
||||
make_generic_validator(test_validator)
|
||||
assert ': (self, v), "self" not permitted as first argument, should be: (cls, value' in str(exc_info.value)
|
||||
|
||||
|
||||
def test_assert_raises_validation_error():
|
||||
class Model(BaseModel):
|
||||
a: str
|
||||
|
||||
@validator('a')
|
||||
def check_a(cls, v):
|
||||
assert v == 'a', 'invalid a'
|
||||
return v
|
||||
|
||||
Model(a='a')
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
Model(a='snap')
|
||||
injected_by_pytest = "\nassert 'snap' == 'a'\n - snap\n + a"
|
||||
assert exc_info.value.errors() == [
|
||||
{'loc': ('a',), 'msg': f'invalid a{injected_by_pytest}', 'type': 'assertion_error'}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
This test is executed separately due to pytest's assertion-rewriting
|
||||
"""
|
||||
from pydantic import BaseModel, ValidationError, validator
|
||||
|
||||
|
||||
def test_assert_raises_validation_error():
|
||||
test_name = test_assert_raises_validation_error.__name__
|
||||
|
||||
class Model(BaseModel):
|
||||
a: str
|
||||
|
||||
@validator('a')
|
||||
def check_a(cls, v):
|
||||
assert v == 'a', 'invalid a'
|
||||
return v
|
||||
|
||||
Model(a='a')
|
||||
expected_errors = [{'loc': ('a',), 'msg': f'invalid a', 'type': 'assertion_error'}]
|
||||
|
||||
try:
|
||||
Model(a='snap')
|
||||
except ValidationError as exc:
|
||||
actual_errors = exc.errors()
|
||||
if actual_errors != expected_errors:
|
||||
raise RuntimeError(f'{test_name}:\nActual errors: {actual_errors}\nExpected errors: {expected_errors}')
|
||||
else:
|
||||
raise RuntimeError(f'{test_name}: ValidationError was not raised')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_assert_raises_validation_error()
|
||||
print('Non-pytest assert tests passed')
|
||||
Reference in New Issue
Block a user