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 e6029f4e55..f0fde031e1 100644 --- a/pydantic/errors.py +++ b/pydantic/errors.py @@ -91,6 +91,17 @@ ) +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 PydanticErrorMixin: code: str msg_template: str @@ -101,6 +112,9 @@ def __init__(self, **ctx: Any) -> None: def __str__(self) -> str: return self.msg_template.format(**self.__dict__) + def __reduce__(self): # type: ignore + 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):