From da159e26a6b2160a8b5e3e7279efd1b199f42b0a Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 26 Mar 2018 07:05:51 -0400 Subject: [PATCH 1/5] CHeckpoint --- Lib/dataclasses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 8ccc4c88aecab0..27aa3c913a649a 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -695,6 +695,8 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # all in the post-processed class. delattr(cls, f.name) else: + if inspect.ismethoddescriptor(f.default): + print('setting method descriptor') setattr(cls, f.name, f.default) # Do we have any Field members that don't also have annotations? From 48a07606d9e44bef642d8972a25cd1c6c9cab828 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 26 Mar 2018 12:28:31 -0400 Subject: [PATCH 2/5] bpo-33141: Add Field.__set_name__ and call the default value, if it exists. This allows the descriptor protocol's __set_name__ to work. --- Lib/dataclasses.py | 29 +++++++++++++++++++++++++++-- Lib/test/test_dataclasses.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 27aa3c913a649a..43f3a0b279d1d8 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -240,6 +240,21 @@ def __repr__(self): f'metadata={self.metadata}' ')') + # This is used to support the PEP 487 __set_name__ protocol in the + # case where we're using a field that contains a descriptor as a + # defaul value. For details on __set_name__, see + # https://www.python.org/dev/peps/pep-0487/#implementation-details. + # Note that in _process_class, this Field object is overwritten with + # the default value, so the end result is a descriptor that had + # __set_name__ called on it at the right time. + def __set_name__(self, owner, name): + if inspect.ismethoddescriptor(self.default): + func = getattr(self.default, '__set_name__', None) + if func: + # There is a __set_name__ method on the descriptor, + # call it. + func(owner, name) + class _DataclassParams: __slots__ = ('init', @@ -695,8 +710,18 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # all in the post-processed class. delattr(cls, f.name) else: - if inspect.ismethoddescriptor(f.default): - print('setting method descriptor') + # Because this class was initially initialized with a + # Field object, if we're overwriting that with a + # method descriptor then the descriptor's + # .__set_name__ method will not have been called at + # class creation time. Manually call __set_name__ + # here, if it exists. + ## if inspect.ismethoddescriptor(f.default): + ## func = getattr(f.default, '__set_name__', None) + ## if func: + ## # There is a __set_name__ method on the + ## # descriptor, call it. + ## func(cls, f.name) setattr(cls, f.name, f.default) # Do we have any Field members that don't also have annotations? diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index f7f132ca30d24c..24e3b02d980e5f 100755 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -2698,6 +2698,35 @@ class Derived(Base): # We can add a new field to the derived instance. d.z = 10 +class TestDescriptors(unittest.TestCase): + def test_set_name(self): + # See bpo-33141. + + # Create a descriptor. + class D: + def __set_name__(self, owner, name): + self.name = name + def __get__(self, instance, owner): + if instance is not None: + return 1 + return self + + # This is the case of just normal descriptor behavior, no + # dataclass code is involved in initializing the descriptor. + @dataclass + class C: + c: int=D() + self.assertEqual(C.c.name, 'c') + + # Now test with a default value and init=False, which is the + # only time this is really meaningful. If not using + # init=False, then the descriptor will be overwritten, anyway. + @dataclass + class C: + c: int=field(default=D(), init=False) + self.assertEqual(C.c.name, 'c') + self.assertEqual(C().c, 1) + if __name__ == '__main__': unittest.main() From 35d32d952337cb56d5f93d89aa4b64300369918d Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 26 Mar 2018 12:31:37 -0400 Subject: [PATCH 3/5] Remove unused code from an early implementation. --- Lib/dataclasses.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 43f3a0b279d1d8..4dd4d4b29d32e4 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -710,18 +710,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): # all in the post-processed class. delattr(cls, f.name) else: - # Because this class was initially initialized with a - # Field object, if we're overwriting that with a - # method descriptor then the descriptor's - # .__set_name__ method will not have been called at - # class creation time. Manually call __set_name__ - # here, if it exists. - ## if inspect.ismethoddescriptor(f.default): - ## func = getattr(f.default, '__set_name__', None) - ## if func: - ## # There is a __set_name__ method on the - ## # descriptor, call it. - ## func(cls, f.name) setattr(cls, f.name, f.default) # Do we have any Field members that don't also have annotations? From 2f2abcc641434cc9bb19f22386fbad6e499ac7cd Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 26 Mar 2018 12:33:26 -0400 Subject: [PATCH 4/5] Added blurb. --- .../next/Library/2018-03-26-12-33-13.bpo-33141.23wlxf.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2018-03-26-12-33-13.bpo-33141.23wlxf.rst diff --git a/Misc/NEWS.d/next/Library/2018-03-26-12-33-13.bpo-33141.23wlxf.rst b/Misc/NEWS.d/next/Library/2018-03-26-12-33-13.bpo-33141.23wlxf.rst new file mode 100644 index 00000000000000..1d49c08fed5449 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-03-26-12-33-13.bpo-33141.23wlxf.rst @@ -0,0 +1,2 @@ +Have Field objects pass through __set_name__ to their default values, if +they have their own __set_name__. From ae95abb4d584f09eddcda62d23ed29cbf1fb115a Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Mon, 26 Mar 2018 13:03:21 -0400 Subject: [PATCH 5/5] Remove restriction that the default for __set_name__ needs to be a descriptor. --- Lib/dataclasses.py | 11 +++++------ Lib/test/test_dataclasses.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 4dd4d4b29d32e4..8c197fe73904d1 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -248,12 +248,11 @@ def __repr__(self): # the default value, so the end result is a descriptor that had # __set_name__ called on it at the right time. def __set_name__(self, owner, name): - if inspect.ismethoddescriptor(self.default): - func = getattr(self.default, '__set_name__', None) - if func: - # There is a __set_name__ method on the descriptor, - # call it. - func(owner, name) + func = getattr(self.default, '__set_name__', None) + if func: + # There is a __set_name__ method on the descriptor, + # call it. + func(owner, name) class _DataclassParams: diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 24e3b02d980e5f..2745eaf6893b74 100755 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -2727,6 +2727,19 @@ class C: self.assertEqual(C.c.name, 'c') self.assertEqual(C().c, 1) + def test_non_descriptor(self): + # PEP 487 says __set_name__ should work on non-descriptors. + # Create a descriptor. + + class D: + def __set_name__(self, owner, name): + self.name = name + + @dataclass + class C: + c: int=field(default=D(), init=False) + self.assertEqual(C.c.name, 'c') + if __name__ == '__main__': unittest.main()