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:
Abdussamet Koçak
2019-08-15 14:06:21 +03:00
committed by Samuel Colvin
parent 321cde0c88
commit f41d5dca3c
9 changed files with 73 additions and 2 deletions
+1
View File
@@ -35,6 +35,7 @@ mypy:
.PHONY: test
test:
pytest --cov=pydantic
@python tests/try_assert.py
.PHONY: external-mypy
external-mypy:
+1
View File
@@ -0,0 +1 @@
add support for ``assert`` statements inside validators
+6
View File
@@ -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
View File
@@ -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``
+2
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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'),
),
)
+19
View File
@@ -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'}
]
+33
View File
@@ -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')