From 4054b172ab59054715a2aaf4979bd84ac23e3ada Mon Sep 17 00:00:00 2001 From: "Aaron Hall, MBA" Date: Sun, 20 May 2018 19:46:42 -0400 Subject: [PATCH] bpo-26103: Fix inspect.isdatadescriptor() and a data descriptor definition. (GH-1959) Look for '__set__' or '__delete__'. --- Doc/howto/descriptor.rst | 2 +- Lib/inspect.py | 4 +- Lib/test/test_inspect.py | 57 ++++++++++++++++++- Misc/ACKS | 1 + .../2018-05-14-09-07-14.bpo-26103._zU8E2.rst | 2 + 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-05-14-09-07-14.bpo-26103._zU8E2.rst diff --git a/Doc/howto/descriptor.rst b/Doc/howto/descriptor.rst index 5e85a9aa6594e4..6e4aa3e975f6f4 100644 --- a/Doc/howto/descriptor.rst +++ b/Doc/howto/descriptor.rst @@ -58,7 +58,7 @@ That is all there is to it. Define any of these methods and an object is considered a descriptor and can override default behavior upon being looked up as an attribute. -If an object defines both :meth:`__get__` and :meth:`__set__`, it is considered +If an object defines :meth:`__set__` or :meth:`__delete__`, it is considered a data descriptor. Descriptors that only define :meth:`__get__` are called non-data descriptors (they are typically used for methods but other uses are possible). diff --git a/Lib/inspect.py b/Lib/inspect.py index 512785f9237ea2..e5d312eb302102 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -110,7 +110,7 @@ def ismethoddescriptor(object): def isdatadescriptor(object): """Return true if the object is a data descriptor. - Data descriptors have both a __get__ and a __set__ attribute. Examples are + Data descriptors have a __set__ or a __delete__ attribute. Examples are properties (defined in Python) and getsets and members (defined in C). Typically, data descriptors will also have __name__ and __doc__ attributes (properties, getsets, and members have both of these attributes), but this @@ -119,7 +119,7 @@ def isdatadescriptor(object): # mutual exclusion return False tp = type(object) - return hasattr(tp, "__set__") and hasattr(tp, "__get__") + return hasattr(tp, "__set__") or hasattr(tp, "__delete__") if hasattr(types, 'MemberDescriptorType'): # CPython and equivalent diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 3481a57ec83360..ee227a66b17c48 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -1134,6 +1134,61 @@ class C(metaclass=M): attrs = [a[0] for a in inspect.getmembers(C)] self.assertNotIn('missing', attrs) +class TestIsDataDescriptor(unittest.TestCase): + + def test_custom_descriptors(self): + class NonDataDescriptor: + def __get__(self, value, type=None): pass + class DataDescriptor0: + def __set__(self, name, value): pass + class DataDescriptor1: + def __delete__(self, name): pass + class DataDescriptor2: + __set__ = None + self.assertFalse(inspect.isdatadescriptor(NonDataDescriptor()), + 'class with only __get__ not a data descriptor') + self.assertTrue(inspect.isdatadescriptor(DataDescriptor0()), + 'class with __set__ is a data descriptor') + self.assertTrue(inspect.isdatadescriptor(DataDescriptor1()), + 'class with __delete__ is a data descriptor') + self.assertTrue(inspect.isdatadescriptor(DataDescriptor2()), + 'class with __set__ = None is a data descriptor') + + def test_slot(self): + class Slotted: + __slots__ = 'foo', + self.assertTrue(inspect.isdatadescriptor(Slotted.foo), + 'a slot is a data descriptor') + + def test_property(self): + class Propertied: + @property + def a_property(self): + pass + self.assertTrue(inspect.isdatadescriptor(Propertied.a_property), + 'a property is a data descriptor') + + def test_functions(self): + class Test(object): + def instance_method(self): pass + @classmethod + def class_method(cls): pass + @staticmethod + def static_method(): pass + def function(): + pass + a_lambda = lambda: None + self.assertFalse(inspect.isdatadescriptor(Test().instance_method), + 'a instance method is not a data descriptor') + self.assertFalse(inspect.isdatadescriptor(Test().class_method), + 'a class method is not a data descriptor') + self.assertFalse(inspect.isdatadescriptor(Test().static_method), + 'a static method is not a data descriptor') + self.assertFalse(inspect.isdatadescriptor(function), + 'a function is not a data descriptor') + self.assertFalse(inspect.isdatadescriptor(a_lambda), + 'a lambda is not a data descriptor') + _global_ref = object() class TestGetClosureVars(unittest.TestCase): @@ -3792,7 +3847,7 @@ def test_main(): TestGetcallargsUnboundMethods, TestGetattrStatic, TestGetGeneratorState, TestNoEOL, TestSignatureObject, TestSignatureBind, TestParameterObject, TestBoundArguments, TestSignaturePrivateHelpers, - TestSignatureDefinitions, + TestSignatureDefinitions, TestIsDataDescriptor, TestGetClosureVars, TestUnwrap, TestMain, TestReload, TestGetCoroutineState ) diff --git a/Misc/ACKS b/Misc/ACKS index cb7f4cd0275bc8..4d295b60a06500 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -603,6 +603,7 @@ Peter Haight Václav Haisman Zbigniew Halas Walker Hale IV +Aaron Christopher Hall Bob Halley Jesse Hallio Jun Hamano diff --git a/Misc/NEWS.d/next/Library/2018-05-14-09-07-14.bpo-26103._zU8E2.rst b/Misc/NEWS.d/next/Library/2018-05-14-09-07-14.bpo-26103._zU8E2.rst new file mode 100644 index 00000000000000..cb4c41ba5db928 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-05-14-09-07-14.bpo-26103._zU8E2.rst @@ -0,0 +1,2 @@ +Correct ``inspect.isdatadescriptor`` to look for ``__set__`` or +``__delete__``. Patch by Aaron Hall.