Fix pickling for instances of classes created using make_class() (#282)

This commit is contained in:
Stanis Trendelenburg 2017-11-04 14:38:03 +01:00 committed by Hynek Schlawack
parent 1bb8fbd5ef
commit c1adf5cfc6
5 changed files with 33 additions and 8 deletions

View File

@ -0,0 +1 @@
Instances of classes created using ``attr.make_class()`` can now be pickled.

View File

@ -601,7 +601,7 @@ You can still have power over the attributes if you pass a dictionary of name: `
... repr=False)
>>> i = C()
>>> i # no repr added!
<attr._make.C object at ...>
<__main__.C object at ...>
>>> i.x
42
>>> i.y

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function
import hashlib
import linecache
import sys
from operator import itemgetter
@ -1237,13 +1238,22 @@ def make_class(name, attrs, bases=(object,), **attributes_arguments):
raise TypeError("attrs argument must be a dict or a list.")
post_init = cls_dict.pop("__attrs_post_init__", None)
return _attrs(
these=cls_dict, **attributes_arguments
)(type(
type_ = type(
name,
bases,
{} if post_init is None else {"__attrs_post_init__": post_init}
))
)
# For pickling to work, the __module__ variable needs to be set to the
# frame where the class is created. Bypass this step in environments where
# sys._getframe is not defined (Jython for example) or sys._getframe is not
# defined for arguments greater than 0 (IronPython)
try:
type_.__module__ = sys._getframe(1).f_globals.get('__name__',
'__main__')
except (AttributeError, ValueError):
pass
return _attrs(these=cls_dict, **attributes_arguments)(type_)
# These are required by within this module so we define them here and merely

View File

@ -103,6 +103,9 @@ class WithMetaSlots(object):
pass
FromMakeClass = attr.make_class('FromMakeClass', ['x'])
class TestDarkMagic(object):
"""
Integration tests.
@ -218,7 +221,8 @@ class TestDarkMagic(object):
@pytest.mark.parametrize("cls",
[C1, C1Slots, C2, C2Slots, Super, SuperSlots,
Sub, SubSlots, Frozen, FrozenNoSlots])
Sub, SubSlots, Frozen, FrozenNoSlots,
FromMakeClass])
@pytest.mark.parametrize("protocol",
range(2, pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_attributes(self, cls, protocol):
@ -230,7 +234,8 @@ class TestDarkMagic(object):
@pytest.mark.parametrize("cls",
[C1, C1Slots, C2, C2Slots, Super, SuperSlots,
Sub, SubSlots, Frozen, FrozenNoSlots])
Sub, SubSlots, Frozen, FrozenNoSlots,
FromMakeClass])
@pytest.mark.parametrize("protocol",
range(2, pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_object(self, cls, protocol):

View File

@ -5,6 +5,7 @@ Tests for `attr._make`.
from __future__ import absolute_import, division, print_function
import inspect
import sys
from operator import attrgetter
@ -456,7 +457,7 @@ class TestMakeClass(object):
attributes_arguments are passed to attributes
"""
C = make_class("C", ["x"], repr=False)
assert repr(C(1)).startswith("<attr._make.C object at 0x")
assert repr(C(1)).startswith("<tests.test_make.C object at 0x")
def test_catches_wrong_attrs_type(self):
"""
@ -494,6 +495,14 @@ class TestMakeClass(object):
assert not isinstance(x, _CountingAttr)
def test_missing_sys_getframe(self, monkeypatch):
"""
`make_class()` does not fail when `sys._getframe()` is not available.
"""
monkeypatch.delattr(sys, '_getframe')
C = make_class("C", ["x"])
assert C.__attrs_attrs__
class TestFields(object):
"""