Skip to content

Commit

Permalink
fix: make pydantic errors (un)pickable (#1630)
Browse files Browse the repository at this point in the history
* fix: make pydantic errors (un)pickable

closes #1616

* add typing

* refactor: rename kwargs into ctx
  • Loading branch information
PrettyWood committed Jun 27, 2020
1 parent 908f6ed commit e5fff9c
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 1 deletion.
1 change: 1 addition & 0 deletions changes/1616-PrettyWood.md
@@ -0,0 +1 @@
make *pydantic* errors (un)pickable
19 changes: 18 additions & 1 deletion 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',
Expand Down Expand Up @@ -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
Expand All @@ -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
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 e5fff9c

Please sign in to comment.