diff --git a/changelog.d/282.change.rst b/changelog.d/282.change.rst new file mode 100644 index 00000000..9e7fc05e --- /dev/null +++ b/changelog.d/282.change.rst @@ -0,0 +1 @@ +Instances of classes created using ``attr.make_class()`` can now be pickled. diff --git a/docs/examples.rst b/docs/examples.rst index 885b368f..67b6e76b 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -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! - + <__main__.C object at ...> >>> i.x 42 >>> i.y diff --git a/src/attr/_make.py b/src/attr/_make.py index eaac0843..06bf6c19 100644 --- a/src/attr/_make.py +++ b/src/attr/_make.py @@ -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 diff --git a/tests/test_dark_magic.py b/tests/test_dark_magic.py index f0f19bef..30d3425f 100644 --- a/tests/test_dark_magic.py +++ b/tests/test_dark_magic.py @@ -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): diff --git a/tests/test_make.py b/tests/test_make.py index 5eb6f13a..a79372b5 100644 --- a/tests/test_make.py +++ b/tests/test_make.py @@ -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("