Skip to content

Commit

Permalink
Fix pickle support for __slots__ classes
Browse files Browse the repository at this point in the history
We add conditionally pickle methods to frozen __slots__ classes and document
that protocol 2 is required for any __slots__ class.

This fixes #81 and fixes #79.
  • Loading branch information
mlongtin0 authored and hynek committed Sep 10, 2016
1 parent dd36808 commit e59c360
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Changes:

- Converts now work with frozen classes.
`#76 <https://github.com/hynek/attrs/issues/76>`_
- Pickling now works with ``__slots__`` classes.
`#81 <https://github.com/hynek/attrs/issues/81>`_


----
Expand Down
7 changes: 7 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,13 @@ Slot classes are a little different than ordinary, dictionary-backed classes:
- Since non-slot classes cannot be turned into slot classes after they have been created, ``attr.s(.., slots=True)`` will *replace* the class it is applied to with a copy.
In almost all cases this isn't a problem, but we mention it for the sake of completeness.

- Using :mod:`pickle` with slot classes requires pickle protocol 2 or greater.
Python 2 uses protocol 0 by default so the protocol needs to be specified.
Python 3 uses protocol 3 by default.
You can support protocol 0 and 1 by implementing :meth:`__getstate__ <object.__getstate__>` and :meth:`__setstate__ <object.__setstate__>` methods yourself.
Those methods are created for frozen slot classes because they won't pickle otherwise.
`Think twice <https://www.youtube.com/watch?v=7KnfGDajDQw>`_ before using :mod:`pickle` though.

All in all, setting ``slots=True`` is usually a very good idea.


Expand Down
40 changes: 40 additions & 0 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ def wrap(cls):
cls = _add_init(cls, frozen)
if frozen is True:
cls.__setattr__ = _frozen_setattrs
if slots is True:
# slots and frozen require __getstate__/__setstate__ to work
cls = _add_pickle(cls)
if slots is True:
cls_dict = dict(cls.__dict__)
cls_dict["__slots__"] = tuple(ca_list)
Expand Down Expand Up @@ -433,6 +436,29 @@ def _add_init(cls, frozen):
return cls


def _add_pickle(cls):
"""
Add pickle helpers, needed for frozen and slotted classes
"""
def _slots_getstate__(obj):
"""
Play nice with pickle.
"""
return tuple(getattr(obj, a.name) for a in fields(obj.__class__))

def _slots_setstate__(obj, state):
"""
Play nice with pickle.
"""
__bound_setattr = _obj_setattr.__get__(obj, Attribute)
for a, value in zip(fields(obj.__class__), state):
__bound_setattr(a.name, value)

cls.__getstate__ = _slots_getstate__
cls.__setstate__ = _slots_setstate__
return cls


def fields(cls):
"""
Returns the tuple of ``attrs`` attributes for a class.
Expand Down Expand Up @@ -630,6 +656,20 @@ def from_counting_attr(cls, name, ca):
in Attribute.__slots__
if k != "name"))

# Don't use _add_pickle since fields(Attribute) doesn't work
def __getstate__(self):
"""
Play nice with pickle.
"""
return tuple(getattr(self, name) for name in self.__slots__)

def __setstate__(self, state):
"""
Play nice with pickle.
"""
__bound_setattr = _obj_setattr.__get__(self, Attribute)
for name, value in zip(self.__slots__, state):
__bound_setattr(name, value)

_a = [Attribute(name=name, default=NOTHING, validator=None,
repr=True, cmp=True, hash=True, init=True)
Expand Down
33 changes: 33 additions & 0 deletions tests/test_dark_magic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import absolute_import, division, print_function
import pickle

import pytest
from hypothesis import given
Expand Down Expand Up @@ -68,6 +69,11 @@ class Frozen(object):
x = attr.ib()


@attr.s(frozen=True, slots=False)
class FrozenNoSlots(object):
x = attr.ib()


class TestDarkMagic(object):
"""
Integration tests.
Expand Down Expand Up @@ -177,3 +183,30 @@ def test_frozen_instance(self, frozen_class):

assert e.value.args[0] == "can't set attribute"
assert 1 == frozen.x

@pytest.mark.parametrize("cls",
[C1, C1Slots, C2, C2Slots, Super, SuperSlots,
Sub, SubSlots, Frozen, FrozenNoSlots])
@pytest.mark.parametrize("protocol",
range(2, pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_attributes(self, cls, protocol):
"""
Pickling/un-pickling of Attribute instances works.
"""
for attribute in attr.fields(cls):
assert attribute == pickle.loads(pickle.dumps(attribute, protocol))

@pytest.mark.parametrize("cls",
[C1, C1Slots, C2, C2Slots, Super, SuperSlots,
Sub, SubSlots, Frozen, FrozenNoSlots])
@pytest.mark.parametrize("protocol",
range(2, pickle.HIGHEST_PROTOCOL + 1))
def test_pickle_object(self, cls, protocol):
"""
Pickle object serialization works on all kinds of attrs classes.
"""
if len(attr.fields(cls)) == 2:
obj = cls(123, 456)
else:
obj = cls(123)
assert repr(obj) == repr(pickle.loads(pickle.dumps(obj, protocol)))

0 comments on commit e59c360

Please sign in to comment.