Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit dacc5ac

Browse filesBrowse 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 <10796600+picnixz@users.noreply.github.com> Co-authored-by: Alyssa Coghlan <ncoghlan@gmail.com>
1 parent 7c5da94 commit dacc5ac
Copy full SHA for dacc5ac

File tree

Expand file treeCollapse file tree

4 files changed

+135
-10
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+135
-10
lines changed

‎Doc/library/inspect.rst

Copy file name to clipboardExpand all lines: Doc/library/inspect.rst
+8-3Lines 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

Copy file name to clipboardExpand all lines: Lib/inspect.py
+7-4Lines 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

Copy file name to clipboardExpand all lines: Lib/test/test_inspect/test_inspect.py
+118-3Lines 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):
+2Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.