Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

T325 weakref with slots #420

Merged
merged 18 commits into from
Aug 25, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/420.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add weakref_slot parameter to ``attr.s()`` allowing for weak referenceable slotted classes.
2 changes: 1 addition & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ What follows is the API explanation, if you'd like a more hands-on introduction,
Core
----

.. autofunction:: attr.s(these=None, repr_ns=None, repr=True, cmp=True, hash=None, init=True, slots=False, frozen=False, str=False, auto_attribs=False, cache_hash=False)
.. autofunction:: attr.s(these=None, repr_ns=None, repr=True, cmp=True, hash=None, init=True, slots=False, frozen=False, weakref_slot=True, str=False, auto_attribs=False, cache_hash=False)

.. note::

Expand Down
15 changes: 3 additions & 12 deletions docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,12 @@ Glossary
Those methods are created for frozen slotted classes because they won't pickle otherwise.
`Think twice <https://www.youtube.com/watch?v=7KnfGDajDQw>`_ before using :mod:`pickle` though.

- As always with slotted classes, you must specify a ``__weakref__`` slot if you wish for the class to be weak-referenceable.
Here's how it looks using ``attrs``:
- Slotted classes are weak-referenceable by default.
This can be disabled in CPython by passing ``weakref_slot=False`` to ``@attr.s`` [#pypyweakref]_.

.. doctest::

>>> import weakref
>>> @attr.s(slots=True)
... class C(object):
... __weakref__ = attr.ib(init=False, hash=False, repr=False, cmp=False)
... x = attr.ib()
>>> c = C(1)
>>> weakref.ref(c)
<weakref at 0x...; to 'C' at 0x...>
- Since it's currently impossible to make a class slotted after it's created, ``attrs`` has to replace your class with a new one.
While it tries to do that as graciously as possible, certain metaclass features like ``__init_subclass__`` do not work with slotted classes.


.. [#pypy] On PyPy, there is no memory advantage in using slotted classes.
.. [#pypyweakref] On PyPy, slotted classes are naturally weak-referenceable so ``weakref_slot=False`` has no effect.
3 changes: 3 additions & 0 deletions src/attr/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ def attrs(
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref_slot: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
Expand All @@ -180,6 +181,7 @@ def attrs(
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref_slot: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
Expand Down Expand Up @@ -207,6 +209,7 @@ def make_class(
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref_slot: bool = ...,
str: bool = ...,
auto_attribs: bool = ...,
kw_only: bool = ...,
Expand Down
46 changes: 41 additions & 5 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,14 +453,23 @@ class _ClassBuilder(object):
"_attr_names",
"_slots",
"_frozen",
"_weakref_slot",
"_cache_hash",
"_has_post_init",
"_delete_attribs",
"_super_attr_map",
)

def __init__(
self, cls, these, slots, frozen, auto_attribs, kw_only, cache_hash
self,
cls,
these,
slots,
frozen,
weakref_slot,
auto_attribs,
kw_only,
cache_hash,
):
attrs, super_attrs, super_map = _transform_attrs(
cls, these, auto_attribs, kw_only
Expand All @@ -474,6 +483,7 @@ def __init__(
self._attr_names = tuple(a.name for a in attrs)
self._slots = slots
self._frozen = frozen or _has_frozen_superclass(cls)
self._weakref_slot = weakref_slot
self._cache_hash = cache_hash
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
self._delete_attribs = not bool(these)
Expand Down Expand Up @@ -531,11 +541,26 @@ def _create_slots_class(self):
if k not in tuple(self._attr_names) + ("__dict__", "__weakref__")
}

weakref_inherited = False

# Traverse the MRO to check for an existing __weakref__.
for super_cls in self._cls.__mro__[1:-1]:
if "__weakref__" in getattr(super_cls, "__dict__", ()):
weakref_inherited = True
break

names = self._attr_names
if (
self._weakref_slot
and "__weakref__" not in getattr(self._cls, "__slots__", ())
and "__weakref__" not in names
and not weakref_inherited
):
names += ("__weakref__",)

# We only add the names of attributes that aren't inherited.
# Settings __slots__ to inherited attributes wastes memory.
slot_names = [
name for name in self._attr_names if name not in super_names
]
slot_names = [name for name in names if name not in super_names]
if self._cache_hash:
slot_names.append(_hash_cache_field)
cd["__slots__"] = tuple(slot_names)
Expand Down Expand Up @@ -678,6 +703,7 @@ def attrs(
init=True,
slots=False,
frozen=False,
weakref_slot=True,
str=False,
auto_attribs=False,
kw_only=False,
Expand Down Expand Up @@ -762,6 +788,8 @@ def attrs(
``object.__setattr__(self, "attribute_name", value)``.

.. _slots: https://docs.python.org/3/reference/datamodel.html#slots
:param bool weakref_slot: Make instances weak-referenceable. This has no
effect unless ``slots`` is also enabled.
:param bool auto_attribs: If True, collect `PEP 526`_-annotated attributes
(Python 3.6 and later only) from the class body.

Expand Down Expand Up @@ -800,6 +828,7 @@ class which participate in hash code computation may be mutated
.. versionchanged:: 18.1.0
If *these* is passed, no attributes are deleted from the class body.
.. versionchanged:: 18.1.0 If *these* is ordered, the order is retained.
.. versionadded:: 18.2.0 *weakref_slot*
.. deprecated:: 18.2.0
``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a
:class:`DeprecationWarning` if the classes compared are subclasses of
Expand All @@ -814,7 +843,14 @@ def wrap(cls):
raise TypeError("attrs only works with new-style classes.")

builder = _ClassBuilder(
cls, these, slots, frozen, auto_attribs, kw_only, cache_hash
cls,
these,
slots,
frozen,
weakref_slot,
auto_attribs,
kw_only,
cache_hash,
)

if repr is True:
Expand Down
23 changes: 16 additions & 7 deletions tests/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,24 +147,26 @@ def simple_attrs_with_metadata(draw):


@st.composite
def simple_classes(draw, slots=None, frozen=None, private_attrs=None):
def simple_classes(
draw, slots=None, frozen=None, weakref_slot=None, private_attrs=None
):
"""
A strategy that generates classes with default non-attr attributes.

For example, this strategy might generate a class such as:

@attr.s(slots=True, frozen=True)
@attr.s(slots=True, frozen=True, weakref_slot=True)
class HypClass:
a = attr.ib(default=1)
_b = attr.ib(default=None)
c = attr.ib(default='text')
_d = attr.ib(default=1.0)
c = attr.ib(default={'t': 1})

By default, all combinations of slots and frozen classes will be generated.
If `slots=True` is passed in, only slots classes will be generated, and
if `slots=False` is passed in, no slot classes will be generated. The same
applies to `frozen`.
By default, all combinations of slots, frozen, and weakref_slot classes
will be generated. If `slots=True` is passed in, only slots classes will be
generated, and if `slots=False` is passed in, no slot classes will be
generated. The same applies to `frozen` and `weakref_slot`.

By default, some attributes will be private (i.e. prefixed with an
underscore). If `private_attrs=True` is passed in, all attributes will be
Expand All @@ -173,6 +175,9 @@ class HypClass:
attrs = draw(list_of_attrs)
frozen_flag = draw(st.booleans()) if frozen is None else frozen
slots_flag = draw(st.booleans()) if slots is None else slots
weakref_slot_flag = (
draw(st.booleans()) if weakref_slot is None else weakref_slot
)

if private_attrs is None:
attr_names = maybe_underscore_prefix(gen_attr_names())
Expand All @@ -191,7 +196,11 @@ def post_init(self):
cls_dict["__attrs_post_init__"] = post_init

return make_class(
"HypClass", cls_dict, slots=slots_flag, frozen=frozen_flag
"HypClass",
cls_dict,
slots=slots_flag,
frozen=frozen_flag,
weakref_slot=weakref_slot_flag,
)


Expand Down
19 changes: 15 additions & 4 deletions tests/test_dark_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,12 +339,13 @@ def compute(self):

@pytest.mark.parametrize("slots", [True, False])
@pytest.mark.parametrize("frozen", [True, False])
def test_attrib_overwrite(self, slots, frozen):
@pytest.mark.parametrize("weakref_slot", [True, False])
def test_attrib_overwrite(self, slots, frozen, weakref_slot):
"""
Subclasses can overwrite attributes of their superclass.
"""

@attr.s(slots=slots, frozen=frozen)
@attr.s(slots=slots, frozen=frozen, weakref_slot=weakref_slot)
class SubOverwrite(Super):
x = attr.ib(default=attr.Factory(list))

Expand Down Expand Up @@ -447,6 +448,8 @@ class E(D):
@pytest.mark.parametrize("sub_slots", [True, False])
@pytest.mark.parametrize("base_frozen", [True, False])
@pytest.mark.parametrize("sub_frozen", [True, False])
@pytest.mark.parametrize("base_weakref_slot", [True, False])
@pytest.mark.parametrize("sub_weakref_slot", [True, False])
@pytest.mark.parametrize("base_converter", [True, False])
@pytest.mark.parametrize("sub_converter", [True, False])
def test_frozen_slots_combo(
Expand All @@ -455,6 +458,8 @@ def test_frozen_slots_combo(
sub_slots,
base_frozen,
sub_frozen,
base_weakref_slot,
sub_weakref_slot,
base_converter,
sub_converter,
):
Expand All @@ -463,11 +468,17 @@ def test_frozen_slots_combo(
with a single attribute.
"""

@attr.s(frozen=base_frozen, slots=base_slots)
@attr.s(
frozen=base_frozen,
slots=base_slots,
weakref_slot=base_weakref_slot,
)
class Base(object):
a = attr.ib(converter=int if base_converter else None)

@attr.s(frozen=sub_frozen, slots=sub_slots)
@attr.s(
frozen=sub_frozen, slots=sub_slots, weakref_slot=sub_weakref_slot
)
class Sub(Base):
b = attr.ib(converter=int if sub_converter else None)

Expand Down
5 changes: 3 additions & 2 deletions tests/test_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -1368,7 +1368,7 @@ def test_repr(self):
class C(object):
pass

b = _ClassBuilder(C, None, True, True, False, False, False)
b = _ClassBuilder(C, None, True, True, False, False, False, False)

assert "<_ClassBuilder(cls=C)>" == repr(b)

Expand All @@ -1380,7 +1380,7 @@ def test_returns_self(self):
class C(object):
x = attr.ib()

b = _ClassBuilder(C, None, True, True, False, False, False)
b = _ClassBuilder(C, None, True, True, False, False, False, False)

cls = (
b.add_cmp()
Expand Down Expand Up @@ -1441,6 +1441,7 @@ class C(object):
these=None,
slots=False,
frozen=False,
weakref_slot=True,
auto_attribs=False,
kw_only=False,
cache_hash=False,
Expand Down
Loading