From e5fff9ccd06165c59d9dfc6126bc4b806ceb14b6 Mon Sep 17 00:00:00 2001 From: PrettyWood Date: Sat, 27 Jun 2020 20:31:23 +0200 Subject: [PATCH] fix: make pydantic errors (un)pickable (#1630) * fix: make pydantic errors (un)pickable closes #1616 * add typing * refactor: rename kwargs into ctx --- changes/1616-PrettyWood.md | 1 + pydantic/errors.py | 19 ++++++++++++++++++- tests/test_errors.py | 13 +++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 changes/1616-PrettyWood.md diff --git a/changes/1616-PrettyWood.md b/changes/1616-PrettyWood.md new file mode 100644 index 0000000000..d56dd2d150 --- /dev/null +++ b/changes/1616-PrettyWood.md @@ -0,0 +1 @@ +make *pydantic* errors (un)pickable \ No newline at end of file diff --git a/pydantic/errors.py b/pydantic/errors.py index c5e4ab927d..c5a4ee9967 100644 --- a/pydantic/errors.py +++ b/pydantic/errors.py @@ -1,9 +1,12 @@ from decimal import Decimal from pathlib import Path -from typing import Any, Set, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Set, Tuple, Type, Union from .typing import display_as_type +if TYPE_CHECKING: + from .typing import DictStrAny + # explicitly state exports to avoid "from .errors import *" also importing Decimal, Path etc. __all__ = ( 'PydanticTypeError', @@ -91,6 +94,17 @@ ) +def cls_kwargs(cls: Type['PydanticErrorMixin'], ctx: 'DictStrAny') -> 'PydanticErrorMixin': + """ + For built-in exceptions like ValueError or TypeError, we need to implement + __reduce__ to override the default behaviour (instead of __getstate__/__setstate__) + By default pickle protocol 2 calls `cls.__new__(cls, *args)`. + Since we only use kwargs, we need a little constructor to change that. + Note: the callable can't be a lambda as pickle looks in the namespace to find it + """ + return cls(**ctx) + + class PydanticErrorMixin: code: str msg_template: str @@ -101,6 +115,9 @@ def __init__(self, **ctx: Any) -> None: def __str__(self) -> str: return self.msg_template.format(**self.__dict__) + def __reduce__(self) -> Tuple[Callable[..., 'PydanticErrorMixin'], Tuple[Type['PydanticErrorMixin'], 'DictStrAny']]: + return cls_kwargs, (self.__class__, self.__dict__) + class PydanticTypeError(PydanticErrorMixin, TypeError): pass diff --git a/tests/test_errors.py b/tests/test_errors.py index 054e28ee44..fd3098f726 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,3 +1,4 @@ +import pickle import sys from typing import Dict, List, Optional, Union from uuid import UUID, uuid4 @@ -6,6 +7,7 @@ from pydantic import UUID1, BaseConfig, BaseModel, PydanticTypeError, ValidationError, conint, errors, validator from pydantic.error_wrappers import flatten_errors, get_exc_type +from pydantic.errors import StrRegexError from pydantic.typing import Literal @@ -22,6 +24,17 @@ def __init__(self, *, test_ctx: int) -> None: assert str(exc_info.value) == 'test message template "test_value"' +def test_pydantic_error_pickable(): + """ + Pydantic errors should be (un)pickable. + (this test does not create a custom local error as we can't pickle local objects) + """ + p = pickle.dumps(StrRegexError(pattern='pika')) + error = pickle.loads(p) + assert isinstance(error, StrRegexError) + assert error.pattern == 'pika' + + @pytest.mark.skipif(not Literal, reason='typing_extensions not installed') def test_interval_validation_error(): class Foo(BaseModel):