diff --git a/changes/1616-PrettyWood.md b/changes/1616-PrettyWood.md new file mode 100644 index 00000000000..d56dd2d1508 --- /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 e6029f4e550..294465d84a9 100644 --- a/pydantic/errors.py +++ b/pydantic/errors.py @@ -3,6 +3,7 @@ from typing import Any, Set, Union from .typing import AnyType, display_as_type +from .utils import PickableError # explicitly state exports to avoid "from .errors import *" also importing Decimal, Path etc. __all__ = ( @@ -91,7 +92,7 @@ ) -class PydanticErrorMixin: +class PydanticErrorMixin(PickableError): code: str msg_template: str diff --git a/pydantic/utils.py b/pydantic/utils.py index fdd4f59a07f..ed1fd98bdbe 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -46,6 +46,7 @@ 'ValueItems', 'version_info', # required here to match behaviour in v1.3 'ClassAttribute', + 'PickableError', ) @@ -283,6 +284,22 @@ def __repr__(self) -> str: return f'{self.__repr_name__()}({self.__repr_str__(", ")})' +def cls_kwargs(cls, kwargs): # type: ignore + """ + 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(**kwargs) + + +class PickableError: + def __reduce__(self): # type: ignore + return cls_kwargs, (self.__class__, self.__dict__) + + class GetterDict(Representation): """ Hack to make object's smell just enough like dicts for validate_model. diff --git a/tests/test_errors.py b/tests/test_errors.py index 054e28ee447..fd3098f7260 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):