From 1c441708ed6edf3e081df9e3a74afba7c6f93703 Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Mon, 14 Jun 2021 23:41:53 +0200 Subject: [PATCH] Change ValidationError serialization to {"loc": [], "msg": ""} --- README.md | 4 +- apischema/__init__.py | 4 +- apischema/deserialization/__init__.py | 46 +++++++------ apischema/graphql/resolvers.py | 12 +--- apischema/validation/errors.py | 71 ++++++++++---------- apischema/validation/validators.py | 44 +++++++----- docs/conversions.md | 3 +- docs/validation.md | 6 +- examples/computed_dependencies.py | 6 +- examples/dependent_required.py | 6 +- examples/discard.py | 8 +-- examples/examples/pydantic_support.py | 14 ++-- examples/field_validator.py | 6 +- examples/not_null.py | 6 +- examples/quickstart.py | 4 +- examples/required.py | 4 +- examples/skip_union.py | 6 +- examples/tagged_union.py | 6 +- examples/validate.py | 4 +- examples/validation_error.py | 17 ++--- examples/validator.py | 6 +- examples/validator_field.py | 6 +- examples/validator_function.py | 6 +- examples/validator_inheritance.py | 6 +- examples/validator_post_init.py | 4 +- examples/validator_yield.py | 8 +-- setup.cfg | 2 +- tests/integration/test_validator_aliasing.py | 21 ++++++ tests/validation/test_validator.py | 5 +- 29 files changed, 176 insertions(+), 165 deletions(-) create mode 100644 tests/integration/test_validator_aliasing.py diff --git a/README.md b/README.md index f2a51042..443c4f37 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ assert serialize(Resource, resource) == data # Validate during deserialization with raises(ValidationError) as err: # pytest checks exception is raised deserialize(Resource, {"id": "42", "name": "wyfo"}) -assert serialize(err.value) == [ # ValidationError is serializable - {"loc": ["id"], "err": ["badly formed hexadecimal UUID string"]} +assert err.value.errors == [ + {"loc": ["id"], "msg": "badly formed hexadecimal UUID string"} ] # Generate JSON Schema assert deserialization_schema(Resource) == { diff --git a/apischema/__init__.py b/apischema/__init__.py index 29a235ec..f8db90a5 100644 --- a/apischema/__init__.py +++ b/apischema/__init__.py @@ -71,8 +71,8 @@ def register_default_conversions(): """Handle standard library + internal types""" from . import std_types # noqa: F401 - deserializer(ValidationError.deserialize) - serializer(ValidationError.serialize) + deserializer(ValidationError.from_errors) + serializer(ValidationError.errors) register_default_conversions() diff --git a/apischema/deserialization/__init__.py b/apischema/deserialization/__init__.py index 10e6d7dd..209913ca 100644 --- a/apischema/deserialization/__init__.py +++ b/apischema/deserialization/__init__.py @@ -126,19 +126,6 @@ def get_constraints(schema: Optional[Schema]) -> Optional[Constraints]: return schema.constraints if schema is not None else None -def with_validators( - method: DeserializationMethod, validators: Sequence[Validator] -) -> DeserializationMethod: - if not validators: - return method - - @wraps(method) - def wrapper(data: Any) -> Any: - return validate(method(data), validators) - - return wrapper - - def get_constraint_errors( constraints: Optional[Constraints], cls: type ) -> Optional[Callable[[Any], Sequence[str]]]: @@ -201,6 +188,19 @@ def annotated( ) return factory + def _with_validators( + self, method: DeserializationMethod, validators: Sequence[Validator] + ) -> DeserializationMethod: + if not validators: + return method + aliaser = self.aliaser + + @wraps(method) + def wrapper(data: Any) -> Any: + return validate(method(data), validators, aliaser=aliaser) + + return wrapper + def any(self) -> DeserializationMethodFactory: def factory( constraints: Optional[Constraints], validators: Sequence[Validator] @@ -214,7 +214,7 @@ def method(data: Any) -> Any: raise ValidationError(errors) return data - return with_validators(method, validators) + return self._with_validators(method, validators) return DeserializationMethodFactory(factory) @@ -244,7 +244,7 @@ def method(data: Any) -> Any: raise ValidationError(errors, elt_errors) return elts if cls is LIST_TYPE else COLLECTION_TYPES[cls](elts) - return with_validators(method, validators) + return self._with_validators(method, validators) return DeserializationMethodFactory(factory, list) @@ -312,7 +312,7 @@ def method(data: Any) -> Any: raise ValidationError(errors, item_errors) return items if cls is DICT_TYPE else MAPPING_TYPES[cls](items) - return with_validators(method, validators) + return self._with_validators(method, validators) return DeserializationMethodFactory(factory, dict) @@ -411,6 +411,7 @@ def factory( flattened_fields or pattern_fields or (additional_field is not None) ) constraint_errors = get_constraint_errors(constraints, dict) + aliaser = self.aliaser def method(data: Any) -> Any: if not isinstance(data, dict): @@ -521,7 +522,12 @@ def method(data: Any) -> Any: if not v.dependencies & invalid_fields ] try: - validate(ValidatorMock(cls, values), validators2, **init) + validate( + ValidatorMock(cls, values), + validators2, + init, + aliaser=aliaser, + ) except ValidationError as err: error = merge_errors(error, err) raise error @@ -541,7 +547,7 @@ def method(data: Any) -> Any: except Exception as err: raise ValidationError([str(err)]) if validators2: - validate(res, validators2, **init) + validate(res, validators2, init, aliaser=aliaser) return res return method @@ -577,7 +583,7 @@ def method(data: Any) -> Any: else: raise bad_type(data, cls) - return with_validators(method, validators) + return self._with_validators(method, validators) return DeserializationMethodFactory(factory, cls) @@ -627,7 +633,7 @@ def method(data: Any) -> Any: raise ValidationError(errors, elt_errors) return tuple(elts) - return with_validators(method, validators) + return self._with_validators(method, validators) return DeserializationMethodFactory(factory, list) diff --git a/apischema/graphql/resolvers.py b/apischema/graphql/resolvers.py index 596bb2b9..9fba00e2 100644 --- a/apischema/graphql/resolvers.py +++ b/apischema/graphql/resolvers.py @@ -26,11 +26,7 @@ from apischema.deserialization import deserialization_method from apischema.objects import ObjectField from apischema.schemas import Schema -from apischema.serialization import ( - SerializationMethod, - SerializationMethodVisitor, - serialize, -) +from apischema.serialization import SerializationMethod, SerializationMethodVisitor from apischema.serialization.serialized_methods import ( ErrorHandler, SerializedMethod, @@ -292,11 +288,7 @@ def resolve(__self, __info, **kwargs): values[param_name] = None if errors: - raise TypeError( - serialize( - ValidationError, ValidationError(children=errors), aliaser=aliaser - ) - ) + raise ValueError(ValidationError(children=errors).errors) if info_parameter: values[info_parameter] = __info try: diff --git a/apischema/validation/errors.py b/apischema/validation/errors.py index 4d641da0..12870227 100644 --- a/apischema/validation/errors.py +++ b/apischema/validation/errors.py @@ -2,12 +2,13 @@ import re import sys import warnings -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from functools import reduce, wraps from inspect import isgeneratorfunction from typing import ( Any, Callable, + Collection, Dict, Generator, Iterable, @@ -19,10 +20,10 @@ Tuple, TypeVar, Union, - cast, overload, ) +from apischema.aliases import Aliaser from apischema.objects import AliasedStr from apischema.typing import get_args, is_annotated from apischema.utils import get_args2, get_origin2, merge_opts @@ -36,22 +37,20 @@ Error = Union[ErrorMsg, Tuple[Any, ErrorMsg]] # where Any = Union[Field, int, str, Iterable[Union[Field, int, str,]]] # but Field being kind of magic not understood by type checkers, it's hidden behind Any -ErrorKey = Union[AliasedStr, str, int] +ErrorKey = Union[str, int] T = TypeVar("T") ValidatorResult = Generator[Error, None, T] +try: + from apischema.typing import TypedDict -@dataclass -class LocalizedError: - loc: Sequence[ErrorKey] - err: Sequence[ErrorMsg] + class LocalizedError(TypedDict): + loc: List[ErrorKey] + msg: ErrorMsg - def nested(self, index=0) -> "ValidationError": - if index == len(self.loc): - return ValidationError(self.err) - else: - assert index < len(self.loc) - return ValidationError(children={self.loc[index]: self.nested(index + 1)}) + +except ImportError: + LocalizedError = Mapping[str, Any] # type: ignore @dataclass @@ -62,20 +61,23 @@ class ValidationError(Exception): def __str__(self): return repr(self) - def flat(self) -> Iterator[Tuple[Tuple[ErrorKey, ...], Sequence[ErrorMsg]]]: - if self.messages: - yield (), self.messages + def _errors(self) -> Iterator[Tuple[List[ErrorKey], ErrorMsg]]: + for msg in self.messages: + yield [], msg for child_key in sorted(self.children): - for path, errors in self.children[child_key].flat(): - yield (child_key, *path), errors + for path, error in self.children[child_key]._errors(): + yield [child_key, *path], error - def serialize(self) -> Sequence[LocalizedError]: - return [LocalizedError(loc, err) for loc, err in self.flat()] + @property + def errors(self) -> List[LocalizedError]: + return [{"loc": path, "msg": error} for path, error in self._errors()] @staticmethod - def deserialize(errors: Sequence[LocalizedError]) -> "ValidationError": + def from_errors(errors: Sequence[LocalizedError]) -> "ValidationError": return reduce( - merge_errors, map(LocalizedError.nested, errors), ValidationError() + merge_errors, + [_rec_build_error(err["loc"], err["msg"]) for err in errors], + ValidationError(), ) @@ -117,8 +119,14 @@ def merge_errors(err1: ValidationError, err2: ValidationError) -> ValidationErro ) -def exception(err: Exception) -> str: - return str(err) +def apply_aliaser(error: ValidationError, aliaser: Aliaser) -> ValidationError: + aliased_children = { + str(aliaser(key)) + if isinstance(key, AliasedStr) + else key: apply_aliaser(child, aliaser) + for key, child in error.children.items() + } + return replace(error, children=aliased_children) def _rec_build_error(path: Sequence[ErrorKey], msg: ErrorMsg) -> ValidationError: @@ -128,17 +136,6 @@ def _rec_build_error(path: Sequence[ErrorKey], msg: ErrorMsg) -> ValidationError return ValidationError(children={path[0]: _rec_build_error(path[1:], msg)}) -def _check_error_path(path) -> Sequence[ErrorKey]: - if isinstance(path, (int, str)): - path = (path,) - for i, elt in enumerate(path): - if not isinstance(path[i], (str, int)): - raise TypeError( - f"Bad error path, expected Field, int or str," f" found {type(i)}" - ) - return cast(Sequence[ErrorKey], path) - - def build_validation_error(errors: Iterable[Error]) -> ValidationError: messages: List[ErrorMsg] = [] children: Dict[ErrorKey, ValidationError] = {} @@ -150,7 +147,9 @@ def build_validation_error(errors: Iterable[Error]) -> ValidationError: if not path: messages.append(msg) else: - key, *remain = _check_error_path(path) + if isinstance(path, str) or not isinstance(path, Collection): + path = (path,) + key, *remain = path children[key] = merge_errors( children.get(key), _rec_build_error(remain, msg) ) diff --git a/apischema/validation/validators.py b/apischema/validation/validators.py index b1dd5a1e..5ab78af1 100644 --- a/apischema/validation/validators.py +++ b/apischema/validation/validators.py @@ -10,6 +10,7 @@ Dict, Iterable, List, + Mapping, Optional, Sequence, Type, @@ -17,8 +18,9 @@ overload, ) +from apischema.aliases import Aliaser from apischema.conversions.dataclass_models import get_model_origin, has_model_origin -from apischema.objects import object_fields +from apischema.objects import get_alias from apischema.objects.fields import FieldOrName, check_field_or_name, get_field_name from apischema.types import AnyType from apischema.typing import get_type_hints @@ -31,6 +33,7 @@ from apischema.validation.dependencies import find_all_dependencies from apischema.validation.errors import ( ValidationError, + apply_aliaser, build_validation_error, merge_errors, ) @@ -116,22 +119,30 @@ def __set_name__(self, owner, name): T = TypeVar("T") -def validate(__obj: T, __validators: Iterable[Validator] = None, **kwargs) -> T: - if __validators is None: - __validators = get_validators(type(__obj)) +def validate( + obj: T, + validators: Iterable[Validator] = None, + kwargs: Mapping[str, Any] = None, + *, + aliaser: Aliaser = lambda s: s, +) -> T: + if validators is None: + validators = get_validators(type(obj)) error: Optional[ValidationError] = None - __validators = iter(__validators) - for validator in __validators: + validators = iter(validators) + for validator in validators: try: - if kwargs and validator.params != kwargs.keys(): + if not kwargs: + validator.validate(obj) + elif validator.params == kwargs.keys(): + validator.validate(obj, **kwargs) + else: if any(k not in kwargs for k in validator.params): raise RuntimeError( f"Missing parameters {kwargs.keys() - validator.params}" f" for validator {validator.func}" ) - validator.validate(__obj, **{k: kwargs[k] for k in validator.params}) - else: - validator.validate(__obj, **kwargs) + validator.validate(obj, **{k: kwargs[k] for k in validator.params}) except ValidationError as e: err = e except NonTrivialDependency as exc: @@ -143,24 +154,23 @@ def validate(__obj: T, __validators: Iterable[Validator] = None, **kwargs) -> T: err = ValidationError([str(e)]) else: continue + err = apply_aliaser(err, aliaser) if validator.field is not None: - alias = object_fields(validator.owner)[ - get_field_name(validator.field) - ].alias - err = ValidationError(children={alias: err}) + alias = getattr(get_alias(validator.owner), get_field_name(validator.field)) + err = ValidationError(children={aliaser(alias): err}) error = merge_errors(error, err) if validator.discard: try: discarded = set(map(get_field_name, validator.discard)) next_validators = ( - v for v in __validators if not discarded & v.dependencies + v for v in validators if not discarded & v.dependencies ) - validate(__obj, next_validators, **kwargs) + validate(obj, next_validators, kwargs, aliaser=aliaser) except ValidationError as err: error = merge_errors(error, err) if error is not None: raise error - return __obj + return obj V = TypeVar("V", bound=Callable) diff --git a/docs/conversions.md b/docs/conversions.md index 2b752939..5a206f42 100644 --- a/docs/conversions.md +++ b/docs/conversions.md @@ -77,8 +77,7 @@ However, it's not allowed to register a conversion of a specialized generic type ## Conversion object -In previous example, conversions where registered using only converter functions. However, everywhere you can pass a converter, you can also pass a `apischema.conversions.Conversion` instance. -`Conversion` allows adding additional metadata to conversion than a function can do ; it can also be used to precise converter source/target when annotations are not available. +In previous example, conversions where registered using only converter functions. However, it can also be done by passing a `apischema.conversions.Conversion` instance. It allows specifying additional metadata to conversion (see [next sections](#sub-conversions) for examples) and precise converter source/target when annotations are not available. ```python {!conversion_object.py!} diff --git a/docs/validation.md b/docs/validation.md index a3fa7414..894c75c8 100644 --- a/docs/validation.md +++ b/docs/validation.md @@ -4,7 +4,7 @@ Validation is an important part of deserialization. By default, *apischema* vali ## Deserialization and validation error -`ValidationError` is raised when validation fails. This exception will contains all the information about the ill-formed part of the data. By the way this exception is also serializable and can be sent back directly to client. +`ValidationError` is raised when validation fails. This exception contains all the information about the ill-formed part of the data. It can be formatted/serialized using its `errors` property. ```python {!validation_error.py!} @@ -13,7 +13,7 @@ Validation is an important part of deserialization. By default, *apischema* vali As shown in the example, *apischema* will not stop at the first error met but tries to validate all parts of the data. !!! note - `ValidationError` should be serialized using the same serializer that the one used for deserialization, because it can contains some unaliased field path + `ValidationError` can also be serialized using `apischema.serialize` (this will use `errors` internally). ## Dataclass validators @@ -57,7 +57,7 @@ In the example, validator yield a tuple of an "error path" and the error message - a field alias (obtained with `apischema.objects.get_alias`); - an integer, for list indices; - a raw string, for dict key (or field); -- an `apischema.objects.AliasedStr`, a string subclass which will be aliased by serialization aliaser; +- an `apischema.objects.AliasedStr`, a string subclass which will be aliased by deserialization aliaser; - an iterable, e.g. a tuple, of this 4 components. `yield` can also be used with only an error message. diff --git a/examples/computed_dependencies.py b/examples/computed_dependencies.py index 85175df5..e0dd8622 100644 --- a/examples/computed_dependencies.py +++ b/examples/computed_dependencies.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize, validator +from apischema import ValidationError, deserialize, validator @dataclass @@ -18,7 +18,7 @@ def password_match(self): with raises(ValidationError) as err: deserialize(PasswordForm, {"password": "p455w0rd"}) -assert serialize(err.value) == [ +assert err.value.errors == [ # validator is not executed because confirmation is missing - {"loc": ["confirmation"], "err": ["missing property"]} + {"loc": ["confirmation"], "msg": "missing property"} ] diff --git a/examples/dependent_required.py b/examples/dependent_required.py index d3050ca5..f9828591 100644 --- a/examples/dependent_required.py +++ b/examples/dependent_required.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, dependent_required, deserialize, serialize +from apischema import ValidationError, dependent_required, deserialize from apischema.json_schema import deserialization_schema from apischema.skip import NotNull @@ -36,9 +36,9 @@ class Billing: with raises(ValidationError) as err: deserialize(Billing, {"name": "Anonymous", "credit_card": 1234_5678_9012_3456}) -assert serialize(err.value) == [ +assert err.value.errors == [ { "loc": ["billing_address"], - "err": ["missing property (required by ['credit_card'])"], + "msg": "missing property (required by ['credit_card'])", } ] diff --git a/examples/discard.py b/examples/discard.py index 1cadfe10..1e0e3931 100644 --- a/examples/discard.py +++ b/examples/discard.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize, validator +from apischema import ValidationError, deserialize, validator from apischema.objects import get_alias, get_field @@ -37,8 +37,8 @@ def bounds_are_sorted_equivalent(bounded: BoundedValues): with raises(ValidationError) as err: deserialize(BoundedValues, {"bounds": [10, 0], "values": [-1, 2, 4]}) -assert serialize(err.value) == [ - {"loc": ["bounds"], "err": ["bounds are not sorted"]} +assert err.value.errors == [ + {"loc": ["bounds"], "msg": "bounds are not sorted"} # Without discard, there would have been an other error - # {"loc": ["values", 1], "err": ["value exceeds bounds"]} + # {"loc": ["values", 1], "msg": "value exceeds bounds"} ] diff --git a/examples/examples/pydantic_support.py b/examples/examples/pydantic_support.py index fba65830..660a93a7 100644 --- a/examples/examples/pydantic_support.py +++ b/examples/examples/pydantic_support.py @@ -16,10 +16,8 @@ from apischema.conversions import AnyConversion, Conversion from apischema.json_schema import deserialization_schema from apischema.schemas import Schema -from apischema.validation.errors import LocalizedError - -#################### Pydantic support code starts here +# ---------- Pydantic support code starts here ---------- prev_deserialization = settings.deserialization.default_conversion @@ -30,9 +28,7 @@ def deserialize_pydantic(data): try: return tp.parse_obj(data) except pydantic.ValidationError as error: - raise ValidationError.deserialize( - [LocalizedError(err["loc"], [err["msg"]]) for err in error.errors()] - ) + raise ValidationError.from_errors(error.errors()) return Conversion( deserialize_pydantic, @@ -65,7 +61,7 @@ def serialize_pydantic(obj: pydantic.BaseModel) -> Any: return getattr(obj, "__root__", obj.dict(exclude_unset=True)) -#################### Pydantic support code ends here +# ---------- Pydantic support code ends here ---------- class Foo(pydantic.BaseModel): @@ -83,6 +79,6 @@ class Foo(pydantic.BaseModel): } with raises(ValidationError) as err: deserialize(Foo, {"bar": "not an int"}) -assert serialize(err.value) == [ - {"loc": ["bar"], "err": ["value is not a valid integer"]} # pydantic error message +assert err.value.errors == [ + {"loc": ["bar"], "msg": "value is not a valid integer"} # pydantic error message ] diff --git a/examples/field_validator.py b/examples/field_validator.py index 8e8b8162..23d3950f 100644 --- a/examples/field_validator.py +++ b/examples/field_validator.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize +from apischema import ValidationError, deserialize from apischema.metadata import validators @@ -18,6 +18,4 @@ class Foo: with raises(ValidationError) as err: deserialize(Foo, {"bar": "11"}) -assert serialize(err.value) == [ - {"loc": ["bar"], "err": ["number has duplicate digits"]} -] +assert err.value.errors == [{"loc": ["bar"], "msg": "number has duplicate digits"}] diff --git a/examples/not_null.py b/examples/not_null.py index 9f468f5a..f5799ca9 100644 --- a/examples/not_null.py +++ b/examples/not_null.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize +from apischema import ValidationError, deserialize from apischema.skip import NotNull @@ -15,6 +15,6 @@ class Foo: with raises(ValidationError) as err: deserialize(Foo, {"bar": None}) -assert serialize(err.value) == [ - {"loc": ["bar"], "err": ["expected type integer, found null"]} +assert err.value.errors == [ + {"loc": ["bar"], "msg": "expected type integer, found null"} ] diff --git a/examples/quickstart.py b/examples/quickstart.py index 4ec306e0..0b17aee7 100644 --- a/examples/quickstart.py +++ b/examples/quickstart.py @@ -30,8 +30,8 @@ class Resource: # Validate during deserialization with raises(ValidationError) as err: # pytest checks exception is raised deserialize(Resource, {"id": "42", "name": "wyfo"}) -assert serialize(err.value) == [ # ValidationError is serializable - {"loc": ["id"], "err": ["badly formed hexadecimal UUID string"]} +assert err.value.errors == [ + {"loc": ["id"], "msg": "badly formed hexadecimal UUID string"} ] # Generate JSON Schema assert deserialization_schema(Resource) == { diff --git a/examples/required.py b/examples/required.py index 719d0ea0..cf68f651 100644 --- a/examples/required.py +++ b/examples/required.py @@ -3,7 +3,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize +from apischema import ValidationError, deserialize from apischema.metadata import required @@ -14,4 +14,4 @@ class Foo: with raises(ValidationError) as err: deserialize(Foo, {}) -assert serialize(err.value) == [{"loc": ["bar"], "err": ["missing property"]}] +assert err.value.errors == [{"loc": ["bar"], "msg": "missing property"}] diff --git a/examples/skip_union.py b/examples/skip_union.py index ce3106df..47ad768f 100644 --- a/examples/skip_union.py +++ b/examples/skip_union.py @@ -3,7 +3,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize +from apischema import ValidationError, deserialize from apischema.skip import Skip @@ -14,6 +14,6 @@ class Foo: with raises(ValidationError) as err: deserialize(Foo, {"bar": None}) -assert serialize(err.value) == [ - {"loc": ["bar"], "err": ["expected type integer, found null"]} +assert err.value.errors == [ + {"loc": ["bar"], "msg": "expected type integer, found null"} ] diff --git a/examples/tagged_union.py b/examples/tagged_union.py index 3839691f..b0b32ebc 100644 --- a/examples/tagged_union.py +++ b/examples/tagged_union.py @@ -34,10 +34,10 @@ class Foo(TaggedUnion): with raises(ValidationError) as err: deserialize(Foo, {"unknown": 42}) -assert serialize(err.value) == [{"loc": ["unknown"], "err": ["unexpected property"]}] +assert err.value.errors == [{"loc": ["unknown"], "msg": "unexpected property"}] with raises(ValidationError) as err: deserialize(Foo, {"bar": {"field": "value"}, "baz": 0}) -assert serialize(err.value) == [ - {"loc": [], "err": ["property count greater than 1 (maxProperties)"]} +assert err.value.errors == [ + {"loc": [], "msg": "property count greater than 1 (maxProperties)"} ] diff --git a/examples/validate.py b/examples/validate.py index 7d3e3f39..412788bc 100644 --- a/examples/validate.py +++ b/examples/validate.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, schema, serialize, validator +from apischema import ValidationError, schema, validator from apischema.validation import validate @@ -22,4 +22,4 @@ def not_equal(self): with raises(ValidationError) as err: validate(Foo(2, 2)) -assert serialize(err.value) == [{"loc": [], "err": ["bar cannot be equal to baz"]}] +assert err.value.errors == [{"loc": [], "msg": "bar cannot be equal to baz"}] diff --git a/examples/validation_error.py b/examples/validation_error.py index 119269de..21df1cc1 100644 --- a/examples/validation_error.py +++ b/examples/validation_error.py @@ -3,7 +3,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, schema, serialize +from apischema import ValidationError, deserialize, schema Tag = NewType("Tag", str) schema(min_len=3, pattern=r"^\w*$", examples=["available", "EMEA"])(Tag) @@ -24,14 +24,9 @@ class Resource: deserialize( Resource, {"id": 42, "tags": ["tag", "duplicate", "duplicate", "bad&", "_"]} ) -assert serialize(err.value) == [ - { - "loc": ["tags"], - "err": [ - "item count greater than 3 (maxItems)", - "duplicate items (uniqueItems)", - ], - }, - {"loc": ["tags", 3], "err": ["not matching '^\\w*$' (pattern)"]}, - {"loc": ["tags", 4], "err": ["string length lower than 3 (minLength)"]}, +assert err.value.errors == [ + {"loc": ["tags"], "msg": "item count greater than 3 (maxItems)"}, + {"loc": ["tags"], "msg": "duplicate items (uniqueItems)"}, + {"loc": ["tags", 3], "msg": "not matching '^\\w*$' (pattern)"}, + {"loc": ["tags", 4], "msg": "string length lower than 3 (minLength)"}, ] diff --git a/examples/validator.py b/examples/validator.py index bbd0092f..5e17a5e5 100644 --- a/examples/validator.py +++ b/examples/validator.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize, validator +from apischema import ValidationError, deserialize, validator @dataclass @@ -19,6 +19,6 @@ def password_match(self): with raises(ValidationError) as err: deserialize(PasswordForm, {"password": "p455w0rd", "confirmation": "..."}) -assert serialize(err.value) == [ - {"loc": [], "err": ["password doesn't match its confirmation"]} +assert err.value.errors == [ + {"loc": [], "msg": "password doesn't match its confirmation"} ] diff --git a/examples/validator_field.py b/examples/validator_field.py index a3fc3912..73d1ee80 100644 --- a/examples/validator_field.py +++ b/examples/validator_field.py @@ -3,7 +3,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize, validator +from apischema import ValidationError, deserialize, validator from apischema.objects import get_alias, get_field @@ -38,6 +38,4 @@ def check_parity_other_equivalent(number2: NumberWithParity): with raises(ValidationError) as err: deserialize(NumberWithParity, {"parity": "even", "number": 1}) -assert serialize(err.value) == [ - {"loc": ["number"], "err": ["number doesn't respect parity"]} -] +assert err.value.errors == [{"loc": ["number"], "msg": "number doesn't respect parity"}] diff --git a/examples/validator_function.py b/examples/validator_function.py index c4c7c72b..723029dc 100644 --- a/examples/validator_function.py +++ b/examples/validator_function.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize, validator +from apischema import ValidationError, deserialize, validator from apischema.metadata import validators Palindrome = NewType("Palindrome", str) @@ -18,9 +18,9 @@ def check_palindrome(s: Palindrome): assert deserialize(Palindrome, "tacocat") == "tacocat" with raises(ValidationError) as err: deserialize(Palindrome, "palindrome") -assert serialize(err.value) == [{"loc": [], "err": ["Not a palindrome"]}] +assert err.value.errors == [{"loc": [], "msg": "Not a palindrome"}] # Using Annotated with raises(ValidationError) as err: deserialize(Annotated[str, validators(check_palindrome)], "palindrom") -assert serialize(err.value) == [{"loc": [], "err": ["Not a palindrome"]}] +assert err.value.errors == [{"loc": [], "msg": "Not a palindrome"}] diff --git a/examples/validator_inheritance.py b/examples/validator_inheritance.py index 5699d8b0..502e1f55 100644 --- a/examples/validator_inheritance.py +++ b/examples/validator_inheritance.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize, validator +from apischema import ValidationError, deserialize, validator @dataclass @@ -26,6 +26,6 @@ class CompleteForm(PasswordForm): CompleteForm, {"username": "wyfo", "password": "p455w0rd", "confirmation": "..."}, ) -assert serialize(err.value) == [ - {"loc": [], "err": ["password doesn't match its confirmation"]} +assert err.value.errors == [ + {"loc": [], "msg": "password doesn't match its confirmation"} ] diff --git a/examples/validator_post_init.py b/examples/validator_post_init.py index 0c9926b9..60685ab1 100644 --- a/examples/validator_post_init.py +++ b/examples/validator_post_init.py @@ -2,7 +2,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize, validator +from apischema import ValidationError, deserialize, validator from apischema.metadata import init_var @@ -18,4 +18,4 @@ def validate(self, bar: int): with raises(ValidationError) as err: deserialize(Foo, {"bar": -1}) -assert serialize(err.value) == [{"loc": ["bar"], "err": ["negative"]}] +assert err.value.errors == [{"loc": ["bar"], "msg": "negative"}] diff --git a/examples/validator_yield.py b/examples/validator_yield.py index 4d4846b5..8da64baa 100644 --- a/examples/validator_yield.py +++ b/examples/validator_yield.py @@ -3,7 +3,7 @@ from pytest import raises -from apischema import ValidationError, deserialize, serialize, validator +from apischema import ValidationError, deserialize, validator from apischema.objects import get_alias @@ -25,7 +25,7 @@ def check_ips_in_subnet(self): SubnetIps, {"subnet": "126.42.18.0/24", "ips": ["126.42.18.1", "126.42.19.0", "0.0.0.0"]}, ) -assert serialize(err.value) == [ - {"loc": ["ips", 1], "err": ["ip not in subnet"]}, - {"loc": ["ips", 2], "err": ["ip not in subnet"]}, +assert err.value.errors == [ + {"loc": ["ips", 1], "msg": "ip not in subnet"}, + {"loc": ["ips", 2], "msg": "ip not in subnet"}, ] diff --git a/setup.cfg b/setup.cfg index 11be1971..14d2ad8d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,3 @@ [flake8] max-line-length = 88 -ignore = E203, E501, W503, E731, E741 +ignore = E203, E302, E501, W503, E731, E741 diff --git a/tests/integration/test_validator_aliasing.py b/tests/integration/test_validator_aliasing.py new file mode 100644 index 00000000..4a4ffe1d --- /dev/null +++ b/tests/integration/test_validator_aliasing.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass, field + +from pytest import raises + +from apischema import ValidationError, deserialize, validator +from apischema.objects import AliasedStr, get_alias + + +@dataclass +class A: + a: int = field() + + @validator(a) + def validate_a(self): + yield (get_alias(self).a, "b", 0, AliasedStr("c")), f"error {self.a}" + + +def test_validator_aliasing(): + with raises(ValidationError) as err: + deserialize(A, {"A": 42}, aliaser=str.upper) + assert err.value.errors == [{"loc": ["A", "A", "b", 0, "C"], "msg": "error 42"}] diff --git a/tests/validation/test_validator.py b/tests/validation/test_validator.py index 46e8ec98..c8874f13 100644 --- a/tests/validation/test_validator.py +++ b/tests/validation/test_validator.py @@ -1,11 +1,9 @@ -from dataclasses import dataclass, field -from operator import not_ +from dataclasses import dataclass from typing import Callable, Type from pytest import raises from apischema import ValidationError, validator -from apischema.metadata import validators from apischema.validation.mock import NonTrivialDependency, ValidatorMock from apischema.validation.validators import Validator, get_validators, validate @@ -15,7 +13,6 @@ class Data: a: int b: int c: int = 0 - with_validator: bool = field(default=False, metadata=validators(not_)) @validator def a_gt_10(self):