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 9 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 parameter to attr.s() allowing for weak referenceable slots classes.

This comment was marked as spam.

This comment was marked as spam.

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)
.. autofunction:: attr.s(these=None, repr_ns=None, repr=True, cmp=True, hash=None, init=True, slots=False, frozen=False, weakref=False, str=False, auto_attribs=False)

.. note::

Expand Down
14 changes: 2 additions & 12 deletions docs/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,11 @@ 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 can be made weak-referenceable in CPython by passing ``weakref=True`` 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=True`` is not needed.
3 changes: 3 additions & 0 deletions src/attr/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ def attrs(
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref: bool = ...,

This comment was marked as spam.

str: bool = ...,
auto_attribs: bool = ...,
) -> _C: ...
Expand All @@ -173,6 +174,7 @@ def attrs(
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref: bool = ...,

This comment was marked as spam.

str: bool = ...,
auto_attribs: bool = ...,
) -> Callable[[_C], _C]: ...
Expand All @@ -198,6 +200,7 @@ def make_class(
init: bool = ...,
slots: bool = ...,
frozen: bool = ...,
weakref: bool = ...,

This comment was marked as spam.

str: bool = ...,
auto_attribs: bool = ...,
) -> type: ...
Expand Down
22 changes: 19 additions & 3 deletions src/attr/_make.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,12 +422,13 @@ class _ClassBuilder(object):
"_attr_names",
"_slots",
"_frozen",
"_weakref",
"_has_post_init",
"_delete_attribs",
"_super_attr_map",
)

def __init__(self, cls, these, slots, frozen, auto_attribs):
def __init__(self, cls, these, slots, frozen, weakref, auto_attribs):
attrs, super_attrs, super_map = _transform_attrs(
cls, these, auto_attribs
)
Expand All @@ -440,6 +441,7 @@ def __init__(self, cls, these, slots, frozen, auto_attribs):
self._attr_names = tuple(a.name for a in attrs)
self._slots = slots
self._frozen = frozen or _has_frozen_superclass(cls)
self._weakref = weakref
self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False))
self._delete_attribs = not bool(these)

Expand Down Expand Up @@ -496,10 +498,18 @@ def _create_slots_class(self):
if k not in tuple(self._attr_names) + ("__dict__", "__weakref__")
}

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

# We only add the names of attributes that aren't inherited.
# Settings __slots__ to inherited attributes wastes memory.
cd["__slots__"] = tuple(
name for name in self._attr_names if name not in super_names
name for name in names if name not in super_names
)

qualname = getattr(self._cls, "__qualname__", None)
Expand Down Expand Up @@ -637,6 +647,7 @@ def attrs(
init=True,
slots=False,
frozen=False,
weakref=False,
str=False,
auto_attribs=False,
):
Expand Down Expand Up @@ -719,6 +730,8 @@ def attrs(
``object.__setattr__(self, "attribute_name", value)``.

.. _slots: https://docs.python.org/3/reference/datamodel.html#slots
:param bool weakref: Make instancess weak-referenceable. This has no

This comment was marked as spam.

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 @@ -747,6 +760,7 @@ def attrs(
.. 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*
.. deprecated:: 18.2.0
``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a
:class:`DeprecationWarning` if the classes compared are subclasses of
Expand All @@ -758,7 +772,9 @@ def wrap(cls):
if getattr(cls, "__class__", None) is None:
raise TypeError("attrs only works with new-style classes.")

builder = _ClassBuilder(cls, these, slots, frozen, auto_attribs)
builder = _ClassBuilder(
cls, these, slots, frozen, weakref, auto_attribs
)

if repr is True:
builder.add_repr(repr_ns)
Expand Down
21 changes: 14 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=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=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 classes will be

This comment was marked as spam.

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`.

This comment was marked as spam.


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,7 @@ 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_flag = draw(st.booleans()) if weakref is None else weakref

if private_attrs is None:
attr_names = maybe_underscore_prefix(gen_attr_names())
Expand All @@ -191,7 +194,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=weakref_flag,
)


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

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

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

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

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

cls = (
b.add_cmp()
Expand Down Expand Up @@ -1222,7 +1222,12 @@ class C(object):
pass

b = _ClassBuilder(
C, these=None, slots=False, frozen=False, auto_attribs=False
C,
these=None,
slots=False,
frozen=False,
weakref=False,
auto_attribs=False,
)
b._cls = {} # no __module__; no __qualname__

Expand Down
98 changes: 98 additions & 0 deletions tests/test_slots.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Unit tests for slot-related functionality.
"""

import weakref

import pytest

import attr
Expand Down Expand Up @@ -430,3 +432,99 @@ def test_missing_ctypes(self, monkeypatch):
) == w.message.args

assert just_warn is func


@pytest.mark.skipif(PYPY, reason="__slots__ only block weakref on CPython")
def test_not_weakrefable():
"""
Instance is not weak-referenceabl when `weakref=False` in CPython.
"""

@attr.s(slots=True)
class C(object):
pass

c = C()

with pytest.raises(TypeError):
weakref.ref(c)


@pytest.mark.skipif(
not PYPY, reason="slots without weakref should only work on PyPy"
)
def test_implicitly_weakrefable():
"""
Instance is weak-referenceabl even when `weakref=False` in PyPy.
"""

@attr.s(slots=True)
class C(object):
pass

c = C()
w = weakref.ref(c)

assert c is w()


def test_weakrefable():
"""
Instance is weak-referenceable when `weakref=True`.
"""

@attr.s(slots=True, weakref=True)
class C(object):
pass

c = C()
w = weakref.ref(c)

assert c is w()


def test_weakref_does_not_add_a_field():
"""
`weakref=True` does not add a field to the class.
"""

@attr.s(slots=True, weakref=True)
class C(object):
field = attr.ib()

assert [f.name for f in attr.fields(C)] == ["field"]


def tests_weakref_does_not_add_when_inheriting_with_weakref():
"""
`weakref=True` does not add a new __weakref__ slot when inheriting one.
"""

@attr.s(slots=True, weakref=True)
class C(object):
pass

@attr.s(slots=True, weakref=True)
class D(C):
pass

d = D()
w = weakref.ref(d)

assert d is w()


def tests_weakref_does_not_add_with_weakref_attribute():
"""
`weakref=True` does not add a new __weakref__ slot when an attribute of
that name exists.
"""

@attr.s(slots=True, weakref=True)
class C(object):
__weakref__ = attr.ib(init=False, hash=False, repr=False, cmp=False)

c = C()
w = weakref.ref(c)

assert c is w()