mirror of
https://github.com/python/cpython.git
synced 2025-07-22 18:55:22 +00:00
gh-91330: Tests and docs for dataclass descriptor-typed fields (GH-94424) (GH-94576)
Co-authored-by: Erik De Bonte <erikd@microsoft.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
(cherry picked from commit 5f319308a8
)
This commit is contained in:
parent
d49c99f10d
commit
552fc9a9ac
3 changed files with 167 additions and 0 deletions
|
@ -749,3 +749,54 @@ mutable types as default values for fields::
|
||||||
``dict``, or ``set``, unhashable objects are now not allowed as
|
``dict``, or ``set``, unhashable objects are now not allowed as
|
||||||
default values. Unhashability is used to approximate
|
default values. Unhashability is used to approximate
|
||||||
mutability.
|
mutability.
|
||||||
|
|
||||||
|
Descriptor-typed fields
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Fields that are assigned :ref:`descriptor objects <descriptors>` as their
|
||||||
|
default value have the following special behaviors:
|
||||||
|
|
||||||
|
* The value for the field passed to the dataclass's ``__init__`` method is
|
||||||
|
passed to the descriptor's ``__set__`` method rather than overwriting the
|
||||||
|
descriptor object.
|
||||||
|
* Similarly, when getting or setting the field, the descriptor's
|
||||||
|
``__get__`` or ``__set__`` method is called rather than returning or
|
||||||
|
overwriting the descriptor object.
|
||||||
|
* To determine whether a field contains a default value, ``dataclasses``
|
||||||
|
will call the descriptor's ``__get__`` method using its class access
|
||||||
|
form (i.e. ``descriptor.__get__(obj=None, type=cls)``. If the
|
||||||
|
descriptor returns a value in this case, it will be used as the
|
||||||
|
field's default. On the other hand, if the descriptor raises
|
||||||
|
:exc:`AttributeError` in this situation, no default value will be
|
||||||
|
provided for the field.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
class IntConversionDescriptor:
|
||||||
|
def __init__(self, *, default):
|
||||||
|
self._default = default
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
self._name = "_" + name
|
||||||
|
|
||||||
|
def __get__(self, obj, type):
|
||||||
|
if obj is None:
|
||||||
|
return self._default
|
||||||
|
|
||||||
|
return getattr(obj, self._name, self._default)
|
||||||
|
|
||||||
|
def __set__(self, obj, value):
|
||||||
|
setattr(obj, self._name, int(value))
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InventoryItem:
|
||||||
|
quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)
|
||||||
|
|
||||||
|
i = InventoryItem()
|
||||||
|
print(i.quantity_on_hand) # 100
|
||||||
|
i.quantity_on_hand = 2.5 # calls __set__ with 2.5
|
||||||
|
print(i.quantity_on_hand) # 2
|
||||||
|
|
||||||
|
Note that if a field is annotated with a descriptor type, but is not assigned
|
||||||
|
a descriptor object as its default value, the field will act like a normal
|
||||||
|
field.
|
||||||
|
|
|
@ -3229,6 +3229,115 @@ class TestDescriptors(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(D.__set_name__.call_count, 1)
|
self.assertEqual(D.__set_name__.call_count, 1)
|
||||||
|
|
||||||
|
def test_init_calls_set(self):
|
||||||
|
class D:
|
||||||
|
pass
|
||||||
|
|
||||||
|
D.__set__ = Mock()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
i: D = D()
|
||||||
|
|
||||||
|
# Make sure D.__set__ is called.
|
||||||
|
D.__set__.reset_mock()
|
||||||
|
c = C(5)
|
||||||
|
self.assertEqual(D.__set__.call_count, 1)
|
||||||
|
|
||||||
|
def test_getting_field_calls_get(self):
|
||||||
|
class D:
|
||||||
|
pass
|
||||||
|
|
||||||
|
D.__set__ = Mock()
|
||||||
|
D.__get__ = Mock()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
i: D = D()
|
||||||
|
|
||||||
|
c = C(5)
|
||||||
|
|
||||||
|
# Make sure D.__get__ is called.
|
||||||
|
D.__get__.reset_mock()
|
||||||
|
value = c.i
|
||||||
|
self.assertEqual(D.__get__.call_count, 1)
|
||||||
|
|
||||||
|
def test_setting_field_calls_set(self):
|
||||||
|
class D:
|
||||||
|
pass
|
||||||
|
|
||||||
|
D.__set__ = Mock()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
i: D = D()
|
||||||
|
|
||||||
|
c = C(5)
|
||||||
|
|
||||||
|
# Make sure D.__set__ is called.
|
||||||
|
D.__set__.reset_mock()
|
||||||
|
c.i = 10
|
||||||
|
self.assertEqual(D.__set__.call_count, 1)
|
||||||
|
|
||||||
|
def test_setting_uninitialized_descriptor_field(self):
|
||||||
|
class D:
|
||||||
|
pass
|
||||||
|
|
||||||
|
D.__set__ = Mock()
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
i: D
|
||||||
|
|
||||||
|
# D.__set__ is not called because there's no D instance to call it on
|
||||||
|
D.__set__.reset_mock()
|
||||||
|
c = C(5)
|
||||||
|
self.assertEqual(D.__set__.call_count, 0)
|
||||||
|
|
||||||
|
# D.__set__ still isn't called after setting i to an instance of D
|
||||||
|
# because descriptors don't behave like that when stored as instance vars
|
||||||
|
c.i = D()
|
||||||
|
c.i = 5
|
||||||
|
self.assertEqual(D.__set__.call_count, 0)
|
||||||
|
|
||||||
|
def test_default_value(self):
|
||||||
|
class D:
|
||||||
|
def __get__(self, instance: Any, owner: object) -> int:
|
||||||
|
if instance is None:
|
||||||
|
return 100
|
||||||
|
|
||||||
|
return instance._x
|
||||||
|
|
||||||
|
def __set__(self, instance: Any, value: int) -> None:
|
||||||
|
instance._x = value
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
i: D = D()
|
||||||
|
|
||||||
|
c = C()
|
||||||
|
self.assertEqual(c.i, 100)
|
||||||
|
|
||||||
|
c = C(5)
|
||||||
|
self.assertEqual(c.i, 5)
|
||||||
|
|
||||||
|
def test_no_default_value(self):
|
||||||
|
class D:
|
||||||
|
def __get__(self, instance: Any, owner: object) -> int:
|
||||||
|
if instance is None:
|
||||||
|
raise AttributeError()
|
||||||
|
|
||||||
|
return instance._x
|
||||||
|
|
||||||
|
def __set__(self, instance: Any, value: int) -> None:
|
||||||
|
instance._x = value
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class C:
|
||||||
|
i: D = D()
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, 'missing 1 required positional argument'):
|
||||||
|
c = C()
|
||||||
|
|
||||||
class TestStringAnnotations(unittest.TestCase):
|
class TestStringAnnotations(unittest.TestCase):
|
||||||
def test_classvar(self):
|
def test_classvar(self):
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
Added more tests for :mod:`dataclasses` to cover behavior with data
|
||||||
|
descriptor-based fields.
|
||||||
|
|
||||||
|
# Write your Misc/NEWS entry below. It should be a simple ReST paragraph. #
|
||||||
|
Don't start with "- Issue #<n>: " or "- gh-issue-<n>: " or that sort of
|
||||||
|
stuff.
|
||||||
|
###########################################################################
|
Loading…
Add table
Add a link
Reference in a new issue