Skip to content

Commit dacc5ac

Browse files
zuopicnixzncoghlan
authored
gh-120381: Fix inspect.ismethoddescriptor() (#120383)
The `inspect.ismethoddescriptor()` function did not check for the lack of `__delete__()` and, consequently, erroneously returned True when applied to *data* descriptors with only `__get__()` and `__delete__()` defined. Co-authored-by: Bénédikt Tran <[email protected]> Co-authored-by: Alyssa Coghlan <[email protected]>
1 parent 7c5da94 commit dacc5ac

File tree

4 files changed

+135
-10
lines changed

4 files changed

+135
-10
lines changed

Doc/library/inspect.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,9 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
504504
are true.
505505

506506
This, for example, is true of ``int.__add__``. An object passing this test
507-
has a :meth:`~object.__get__` method but not a :meth:`~object.__set__`
508-
method, but beyond that the set of attributes varies. A
509-
:attr:`~definition.__name__` attribute is usually
507+
has a :meth:`~object.__get__` method, but not a :meth:`~object.__set__`
508+
method or a :meth:`~object.__delete__` method. Beyond that, the set of
509+
attributes varies. A :attr:`~definition.__name__` attribute is usually
510510
sensible, and :attr:`!__doc__` often is.
511511

512512
Methods implemented via descriptors that also pass one of the other tests
@@ -515,6 +515,11 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
515515
:attr:`~method.__func__` attribute (etc) when an object passes
516516
:func:`ismethod`.
517517

518+
.. versionchanged:: 3.13
519+
This function no longer incorrectly reports objects with :meth:`~object.__get__`
520+
and :meth:`~object.__delete__`, but not :meth:`~object.__set__`, as being method
521+
descriptors (such objects are data descriptors, not method descriptors).
522+
518523

519524
.. function:: isdatadescriptor(object)
520525

Lib/inspect.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,10 @@ def ismethoddescriptor(object):
307307
But not if ismethod() or isclass() or isfunction() are true.
308308
309309
This is new in Python 2.2, and, for example, is true of int.__add__.
310-
An object passing this test has a __get__ attribute but not a __set__
311-
attribute, but beyond that the set of attributes varies. __name__ is
312-
usually sensible, and __doc__ often is.
310+
An object passing this test has a __get__ attribute, but not a
311+
__set__ attribute or a __delete__ attribute. Beyond that, the set
312+
of attributes varies; __name__ is usually sensible, and __doc__
313+
often is.
313314
314315
Methods implemented via descriptors that also pass one of the other
315316
tests return false from the ismethoddescriptor() test, simply because
@@ -319,7 +320,9 @@ def ismethoddescriptor(object):
319320
# mutual exclusion
320321
return False
321322
tp = type(object)
322-
return hasattr(tp, "__get__") and not hasattr(tp, "__set__")
323+
return (hasattr(tp, "__get__")
324+
and not hasattr(tp, "__set__")
325+
and not hasattr(tp, "__delete__"))
323326

324327
def isdatadescriptor(object):
325328
"""Return true if the object is a data descriptor.

Lib/test/test_inspect/test_inspect.py

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,8 @@
5555
# ismodule, isclass, ismethod, isfunction, istraceback, isframe, iscode,
5656
# isbuiltin, isroutine, isgenerator, isgeneratorfunction, getmembers,
5757
# getdoc, getfile, getmodule, getsourcefile, getcomments, getsource,
58-
# getclasstree, getargvalues, formatargvalues,
59-
# currentframe, stack, trace, isdatadescriptor,
60-
# ismethodwrapper
58+
# getclasstree, getargvalues, formatargvalues, currentframe,
59+
# stack, trace, ismethoddescriptor, isdatadescriptor, ismethodwrapper
6160

6261
# NOTE: There are some additional tests relating to interaction with
6362
# zipimport in the test_zipimport_support test module.
@@ -179,6 +178,7 @@ def test_excluding_predicates(self):
179178
self.istest(inspect.ismethod, 'git.argue')
180179
self.istest(inspect.ismethod, 'mod.custom_method')
181180
self.istest(inspect.ismodule, 'mod')
181+
self.istest(inspect.ismethoddescriptor, 'int.__add__')
182182
self.istest(inspect.isdatadescriptor, 'collections.defaultdict.default_factory')
183183
self.istest(inspect.isgenerator, '(x for x in range(2))')
184184
self.istest(inspect.isgeneratorfunction, 'generator_function_example')
@@ -1813,6 +1813,121 @@ def test_typing_replacement(self):
18131813
self.assertEqual(inspect.formatannotation(ann1), 'Union[List[testModule.typing.A], int]')
18141814

18151815

1816+
class TestIsMethodDescriptor(unittest.TestCase):
1817+
1818+
def test_custom_descriptors(self):
1819+
class MethodDescriptor:
1820+
def __get__(self, *_): pass
1821+
class MethodDescriptorSub(MethodDescriptor):
1822+
pass
1823+
class DataDescriptorWithNoGet:
1824+
def __set__(self, *_): pass
1825+
class DataDescriptorWithGetSet:
1826+
def __get__(self, *_): pass
1827+
def __set__(self, *_): pass
1828+
class DataDescriptorWithGetDelete:
1829+
def __get__(self, *_): pass
1830+
def __delete__(self, *_): pass
1831+
class DataDescriptorSub(DataDescriptorWithNoGet,
1832+
DataDescriptorWithGetDelete):
1833+
pass
1834+
1835+
# Custom method descriptors:
1836+
self.assertTrue(
1837+
inspect.ismethoddescriptor(MethodDescriptor()),
1838+
'__get__ and no __set__/__delete__ => method descriptor')
1839+
self.assertTrue(
1840+
inspect.ismethoddescriptor(MethodDescriptorSub()),
1841+
'__get__ (inherited) and no __set__/__delete__'
1842+
' => method descriptor')
1843+
1844+
# Custom data descriptors:
1845+
self.assertFalse(
1846+
inspect.ismethoddescriptor(DataDescriptorWithNoGet()),
1847+
'__set__ (and no __get__) => not a method descriptor')
1848+
self.assertFalse(
1849+
inspect.ismethoddescriptor(DataDescriptorWithGetSet()),
1850+
'__get__ and __set__ => not a method descriptor')
1851+
self.assertFalse(
1852+
inspect.ismethoddescriptor(DataDescriptorWithGetDelete()),
1853+
'__get__ and __delete__ => not a method descriptor')
1854+
self.assertFalse(
1855+
inspect.ismethoddescriptor(DataDescriptorSub()),
1856+
'__get__, __set__ and __delete__ => not a method descriptor')
1857+
1858+
# Classes of descriptors (are *not* descriptors themselves):
1859+
self.assertFalse(inspect.ismethoddescriptor(MethodDescriptor))
1860+
self.assertFalse(inspect.ismethoddescriptor(MethodDescriptorSub))
1861+
self.assertFalse(inspect.ismethoddescriptor(DataDescriptorSub))
1862+
1863+
def test_builtin_descriptors(self):
1864+
builtin_slot_wrapper = int.__add__ # This one is mentioned in docs.
1865+
class Owner:
1866+
def instance_method(self): pass
1867+
@classmethod
1868+
def class_method(cls): pass
1869+
@staticmethod
1870+
def static_method(): pass
1871+
@property
1872+
def a_property(self): pass
1873+
class Slotermeyer:
1874+
__slots__ = 'a_slot',
1875+
def function():
1876+
pass
1877+
a_lambda = lambda: None
1878+
1879+
# Example builtin method descriptors:
1880+
self.assertTrue(
1881+
inspect.ismethoddescriptor(builtin_slot_wrapper),
1882+
'a builtin slot wrapper is a method descriptor')
1883+
self.assertTrue(
1884+
inspect.ismethoddescriptor(Owner.__dict__['class_method']),
1885+
'a classmethod object is a method descriptor')
1886+
self.assertTrue(
1887+
inspect.ismethoddescriptor(Owner.__dict__['static_method']),
1888+
'a staticmethod object is a method descriptor')
1889+
1890+
# Example builtin data descriptors:
1891+
self.assertFalse(
1892+
inspect.ismethoddescriptor(Owner.__dict__['a_property']),
1893+
'a property is not a method descriptor')
1894+
self.assertFalse(
1895+
inspect.ismethoddescriptor(Slotermeyer.__dict__['a_slot']),
1896+
'a slot is not a method descriptor')
1897+
1898+
# `types.MethodType`/`types.FunctionType` instances (they *are*
1899+
# method descriptors, but `ismethoddescriptor()` explicitly
1900+
# excludes them):
1901+
self.assertFalse(inspect.ismethoddescriptor(Owner().instance_method))
1902+
self.assertFalse(inspect.ismethoddescriptor(Owner().class_method))
1903+
self.assertFalse(inspect.ismethoddescriptor(Owner().static_method))
1904+
self.assertFalse(inspect.ismethoddescriptor(Owner.instance_method))
1905+
self.assertFalse(inspect.ismethoddescriptor(Owner.class_method))
1906+
self.assertFalse(inspect.ismethoddescriptor(Owner.static_method))
1907+
self.assertFalse(inspect.ismethoddescriptor(function))
1908+
self.assertFalse(inspect.ismethoddescriptor(a_lambda))
1909+
1910+
def test_descriptor_being_a_class(self):
1911+
class MethodDescriptorMeta(type):
1912+
def __get__(self, *_): pass
1913+
class ClassBeingMethodDescriptor(metaclass=MethodDescriptorMeta):
1914+
pass
1915+
# `ClassBeingMethodDescriptor` itself *is* a method descriptor,
1916+
# but it is *also* a class, and `ismethoddescriptor()` explicitly
1917+
# excludes classes.
1918+
self.assertFalse(
1919+
inspect.ismethoddescriptor(ClassBeingMethodDescriptor),
1920+
'classes (instances of type) are explicitly excluded')
1921+
1922+
def test_non_descriptors(self):
1923+
class Test:
1924+
pass
1925+
self.assertFalse(inspect.ismethoddescriptor(Test()))
1926+
self.assertFalse(inspect.ismethoddescriptor(Test))
1927+
self.assertFalse(inspect.ismethoddescriptor([42]))
1928+
self.assertFalse(inspect.ismethoddescriptor(42))
1929+
1930+
18161931
class TestIsDataDescriptor(unittest.TestCase):
18171932

18181933
def test_custom_descriptors(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Correct :func:`inspect.ismethoddescriptor` to check also for the lack of
2+
:meth:`~object.__delete__`. Patch by Jan Kaliszewski.

0 commit comments

Comments
 (0)