Add weakref_slot to dataclass decorator, to allow instances with slots to be weakref-able. (#92160)

This commit is contained in:
Eric V. Smith 2022-05-02 10:36:39 -06:00 committed by GitHub
parent 958f21c5cd
commit 5f9c0f5ddf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 106 additions and 13 deletions

View file

@ -46,7 +46,7 @@ directly specified in the ``InventoryItem`` definition shown above.
Module contents Module contents
--------------- ---------------
.. decorator:: dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False) .. decorator:: dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)
This function is a :term:`decorator` that is used to add generated This function is a :term:`decorator` that is used to add generated
:term:`special method`\s to classes, as described below. :term:`special method`\s to classes, as described below.
@ -79,7 +79,7 @@ Module contents
class C: class C:
... ...
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False) @dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)
class C: class C:
... ...
@ -198,6 +198,13 @@ Module contents
base class ``__slots__`` may be any iterable, but *not* an iterator. base class ``__slots__`` may be any iterable, but *not* an iterator.
- ``weakref_slot``: If true (the default is ``False``), add a slot
named "__weakref__", which is required to make an instance
weakref-able. It is an error to specify ``weakref_slot=True``
without also specifying ``slots=True``.
.. versionadded:: 3.11
``field``\s may optionally specify a default value, using normal ``field``\s may optionally specify a default value, using normal
Python syntax:: Python syntax::
@ -381,7 +388,7 @@ Module contents
:func:`astuple` raises :exc:`TypeError` if ``obj`` is not a dataclass :func:`astuple` raises :exc:`TypeError` if ``obj`` is not a dataclass
instance. instance.
.. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False) .. function:: make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False)
Creates a new dataclass with name ``cls_name``, fields as defined Creates a new dataclass with name ``cls_name``, fields as defined
in ``fields``, base classes as given in ``bases``, and initialized in ``fields``, base classes as given in ``bases``, and initialized
@ -390,8 +397,8 @@ Module contents
or ``(name, type, Field)``. If just ``name`` is supplied, or ``(name, type, Field)``. If just ``name`` is supplied,
``typing.Any`` is used for ``type``. The values of ``init``, ``typing.Any`` is used for ``type``. The values of ``init``,
``repr``, ``eq``, ``order``, ``unsafe_hash``, ``frozen``, ``repr``, ``eq``, ``order``, ``unsafe_hash``, ``frozen``,
``match_args``, ``kw_only``, and ``slots`` have the same meaning as ``match_args``, ``kw_only``, ``slots``, and ``weakref_slot`` have
they do in :func:`dataclass`. the same meaning as they do in :func:`dataclass`.
This function is not strictly required, because any Python This function is not strictly required, because any Python
mechanism for creating a new class with ``__annotations__`` can mechanism for creating a new class with ``__annotations__`` can

View file

@ -883,7 +883,7 @@ _hash_action = {(False, False, False, False): None,
def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
match_args, kw_only, slots): match_args, kw_only, slots, weakref_slot):
# Now that dicts retain insertion order, there's no reason to use # Now that dicts retain insertion order, there's no reason to use
# an ordered dict. I am leveraging that ordering here, because # an ordered dict. I am leveraging that ordering here, because
# derived class fields overwrite base class fields, but the order # derived class fields overwrite base class fields, but the order
@ -1101,8 +1101,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
_set_new_attribute(cls, '__match_args__', _set_new_attribute(cls, '__match_args__',
tuple(f.name for f in std_init_fields)) tuple(f.name for f in std_init_fields))
# It's an error to specify weakref_slot if slots is False.
if weakref_slot and not slots:
raise TypeError('weakref_slot is True but slots is False')
if slots: if slots:
cls = _add_slots(cls, frozen) cls = _add_slots(cls, frozen, weakref_slot)
abc.update_abstractmethods(cls) abc.update_abstractmethods(cls)
@ -1137,7 +1140,7 @@ def _get_slots(cls):
raise TypeError(f"Slots of '{cls.__name__}' cannot be determined") raise TypeError(f"Slots of '{cls.__name__}' cannot be determined")
def _add_slots(cls, is_frozen): def _add_slots(cls, is_frozen, weakref_slot):
# Need to create a new class, since we can't set __slots__ # Need to create a new class, since we can't set __slots__
# after a class has been created. # after a class has been created.
@ -1152,9 +1155,14 @@ def _add_slots(cls, is_frozen):
inherited_slots = set( inherited_slots = set(
itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1])) itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
) )
# The slots for our class. Remove slots from our base classes. Add
# '__weakref__' if weakref_slot was given.
cls_dict["__slots__"] = tuple( cls_dict["__slots__"] = tuple(
itertools.filterfalse(inherited_slots.__contains__, field_names) itertools.chain(
itertools.filterfalse(inherited_slots.__contains__, field_names),
("__weakref__",) if weakref_slot else ())
) )
for field_name in field_names: for field_name in field_names:
# Remove our attributes, if present. They'll still be # Remove our attributes, if present. They'll still be
# available in _MARKER. # available in _MARKER.
@ -1179,7 +1187,7 @@ def _add_slots(cls, is_frozen):
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False, match_args=True, unsafe_hash=False, frozen=False, match_args=True,
kw_only=False, slots=False): kw_only=False, slots=False, weakref_slot=False):
"""Returns the same class as was passed in, with dunder methods """Returns the same class as was passed in, with dunder methods
added based on the fields defined in the class. added based on the fields defined in the class.
@ -1197,7 +1205,8 @@ def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False,
def wrap(cls): def wrap(cls):
return _process_class(cls, init, repr, eq, order, unsafe_hash, return _process_class(cls, init, repr, eq, order, unsafe_hash,
frozen, match_args, kw_only, slots) frozen, match_args, kw_only, slots,
weakref_slot)
# See if we're being called as @dataclass or @dataclass(). # See if we're being called as @dataclass or @dataclass().
if cls is None: if cls is None:
@ -1356,7 +1365,8 @@ def _astuple_inner(obj, tuple_factory):
def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
repr=True, eq=True, order=False, unsafe_hash=False, repr=True, eq=True, order=False, unsafe_hash=False,
frozen=False, match_args=True, kw_only=False, slots=False): frozen=False, match_args=True, kw_only=False, slots=False,
weakref_slot=False):
"""Return a new dynamically created dataclass. """Return a new dynamically created dataclass.
The dataclass name will be 'cls_name'. 'fields' is an iterable The dataclass name will be 'cls_name'. 'fields' is an iterable
@ -1423,7 +1433,8 @@ def make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True,
# Apply the normal decorator. # Apply the normal decorator.
return dataclass(cls, init=init, repr=repr, eq=eq, order=order, return dataclass(cls, init=init, repr=repr, eq=eq, order=order,
unsafe_hash=unsafe_hash, frozen=frozen, unsafe_hash=unsafe_hash, frozen=frozen,
match_args=match_args, kw_only=kw_only, slots=slots) match_args=match_args, kw_only=kw_only, slots=slots,
weakref_slot=weakref_slot)
def replace(obj, /, **changes): def replace(obj, /, **changes):

View file

@ -9,6 +9,7 @@ import pickle
import inspect import inspect
import builtins import builtins
import types import types
import weakref
import unittest import unittest
from unittest.mock import Mock from unittest.mock import Mock
from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional, Protocol
@ -3038,6 +3039,77 @@ class TestSlots(unittest.TestCase):
self.assertEqual(obj.a, 'a') self.assertEqual(obj.a, 'a')
self.assertEqual(obj.b, 'b') self.assertEqual(obj.b, 'b')
def test_slots_no_weakref(self):
@dataclass(slots=True)
class A:
# No weakref.
pass
self.assertNotIn("__weakref__", A.__slots__)
a = A()
with self.assertRaisesRegex(TypeError,
"cannot create weak reference"):
weakref.ref(a)
def test_slots_weakref(self):
@dataclass(slots=True, weakref_slot=True)
class A:
a: int
self.assertIn("__weakref__", A.__slots__)
a = A(1)
weakref.ref(a)
def test_slots_weakref_base_str(self):
class Base:
__slots__ = '__weakref__'
@dataclass(slots=True)
class A(Base):
a: int
# __weakref__ is in the base class, not A. But an A is still weakref-able.
self.assertIn("__weakref__", Base.__slots__)
self.assertNotIn("__weakref__", A.__slots__)
a = A(1)
weakref.ref(a)
def test_slots_weakref_base_tuple(self):
# Same as test_slots_weakref_base, but use a tuple instead of a string
# in the base class.
class Base:
__slots__ = ('__weakref__',)
@dataclass(slots=True)
class A(Base):
a: int
# __weakref__ is in the base class, not A. But an A is still
# weakref-able.
self.assertIn("__weakref__", Base.__slots__)
self.assertNotIn("__weakref__", A.__slots__)
a = A(1)
weakref.ref(a)
def test_weakref_slot_without_slot(self):
with self.assertRaisesRegex(TypeError,
"weakref_slot is True but slots is False"):
@dataclass(weakref_slot=True)
class A:
a: int
def test_weakref_slot_make_dataclass(self):
A = make_dataclass('A', [('a', int),], slots=True, weakref_slot=True)
self.assertIn("__weakref__", A.__slots__)
a = A(1)
weakref.ref(a)
# And make sure if raises if slots=True is not given.
with self.assertRaisesRegex(TypeError,
"weakref_slot is True but slots is False"):
B = make_dataclass('B', [('a', int),], weakref_slot=True)
class TestDescriptors(unittest.TestCase): class TestDescriptors(unittest.TestCase):
def test_set_name(self): def test_set_name(self):
# See bpo-33141. # See bpo-33141.

View file

@ -0,0 +1,3 @@
For @dataclass, add weakref_slot. Default is False. If True, and if
slots=True, add a slot named "__weakref__", which will allow instances to be
weakref'd. Contributed by Eric V. Smith