Skip to content

Commit b1bb6e0

Browse files
Fix #2111: support pickle for built-in dataclasses (#2114)
* 2111: support pickle for built-in dataclasses * 2111: add changes * 2111: simplify test * return original name + handle similar names * add additional check * fix a misspell * remove useless f-string * cleanup test Co-authored-by: Samuel Colvin <[email protected]>
1 parent 9ae40a2 commit b1bb6e0

File tree

3 files changed

+54
-2
lines changed

3 files changed

+54
-2
lines changed

changes/2111-aimestereo.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow pickling of `pydantic.dataclasses.dataclass` dynamically created from a built-in `dataclasses.dataclass`.

pydantic/dataclasses.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,23 @@ def _pydantic_post_init(self: 'Dataclass', *initvars: Any) -> None:
119119
# __post_init__ = _pydantic_post_init
120120
# ```
121121
# with the exact same fields as the base dataclass
122+
# and register it on module level to address pickle problem:
123+
# https://github.com/samuelcolvin/pydantic/issues/2111
122124
if is_builtin_dataclass(_cls):
125+
uniq_class_name = f'_Pydantic_{_cls.__name__}_{id(_cls)}'
123126
_cls = type(
124-
_cls.__name__, (_cls,), {'__annotations__': _cls.__annotations__, '__post_init__': _pydantic_post_init}
127+
# for pretty output new class will have the name as original
128+
_cls.__name__,
129+
(_cls,),
130+
{
131+
'__annotations__': _cls.__annotations__,
132+
'__post_init__': _pydantic_post_init,
133+
# attrs for pickle to find this class
134+
'__module__': __name__,
135+
'__qualname__': uniq_class_name,
136+
},
125137
)
138+
globals()[uniq_class_name] = _cls
126139
else:
127140
_cls.__post_init__ = _pydantic_post_init
128141
cls: Type['Dataclass'] = dataclasses.dataclass( # type: ignore

tests/test_dataclasses.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import dataclasses
2+
import pickle
23
from collections.abc import Hashable
34
from datetime import datetime
45
from pathlib import Path
@@ -733,7 +734,10 @@ class File:
733734
'type': 'object',
734735
}
735736
},
736-
'properties': {'filename': {'title': 'Filename', 'type': 'string'}, 'meta': {'$ref': '#/definitions/Meta'}},
737+
'properties': {
738+
'filename': {'title': 'Filename', 'type': 'string'},
739+
'meta': {'$ref': '#/definitions/Meta'},
740+
},
737741
'required': ['filename', 'meta'],
738742
'title': 'File',
739743
'type': 'object',
@@ -795,3 +799,37 @@ class Config:
795799
e.other = 'bulbi2'
796800
with pytest.raises(dataclasses.FrozenInstanceError):
797801
e.item.name = 'pika2'
802+
803+
804+
def test_pickle_overriden_builtin_dataclass(create_module):
805+
module = create_module(
806+
# language=Python
807+
"""\
808+
import dataclasses
809+
import pydantic
810+
811+
812+
@dataclasses.dataclass
813+
class BuiltInDataclassForPickle:
814+
value: int
815+
816+
class ModelForPickle(pydantic.BaseModel):
817+
# pickle can only work with top level classes as it imports them
818+
819+
dataclass: BuiltInDataclassForPickle
820+
821+
class Config:
822+
validate_assignment = True
823+
"""
824+
)
825+
obj = module.ModelForPickle(dataclass=module.BuiltInDataclassForPickle(value=5))
826+
827+
pickled_obj = pickle.dumps(obj)
828+
restored_obj = pickle.loads(pickled_obj)
829+
830+
assert restored_obj.dataclass.value == 5
831+
assert restored_obj == obj
832+
833+
# ensure the restored dataclass is still a pydantic dataclass
834+
with pytest.raises(ValidationError, match='value\n +value is not a valid integer'):
835+
restored_obj.dataclass.value = 'value of a wrong type'

0 commit comments

Comments
 (0)