Skip to content

Commit

Permalink
fix: make pydantic errors (un)pickable
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyWood committed Jun 13, 2020
1 parent 329b1d3 commit 8a82658
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 1 deletion.
1 change: 1 addition & 0 deletions changes/1616-PrettyWood.md
@@ -0,0 +1 @@
make *pydantic* errors (un)pickable
3 changes: 2 additions & 1 deletion pydantic/errors.py
Expand Up @@ -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__ = (
Expand Down Expand Up @@ -91,7 +92,7 @@
)


class PydanticErrorMixin:
class PydanticErrorMixin(PickableError):
code: str
msg_template: str

Expand Down
17 changes: 17 additions & 0 deletions pydantic/utils.py
Expand Up @@ -46,6 +46,7 @@
'ValueItems',
'version_info', # required here to match behaviour in v1.3
'ClassAttribute',
'PickableError',
)


Expand Down Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions tests/test_errors.py
@@ -1,3 +1,4 @@
import pickle
import sys
from typing import Dict, List, Optional, Union
from uuid import UUID, uuid4
Expand All @@ -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


Expand All @@ -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):
Expand Down

0 comments on commit 8a82658

Please sign in to comment.