Repository navigation
gh-120381: Fix inspect.ismethoddescriptor() #120383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
dcc561b
7740b0e
2dc2168
4f13243
7fdc95a
52f579f
57875a7
01178fa
71c3735
2daa16f
0c7bb3f
84aed74
0390f85
bcfeeb6
eb4d852
8ccb761
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -301,9 +301,10 @@ def ismethoddescriptor(object): | |||||||||
| But not if ismethod() or isclass() or isfunction() are true. | ||||||||||
|
|
||||||||||
| This is new in Python 2.2, and, for example, is true of int.__add__. | ||||||||||
| An object passing this test has a __get__ attribute but not a __set__ | ||||||||||
| attribute, but beyond that the set of attributes varies. __name__ is | ||||||||||
| usually sensible, and __doc__ often is. | ||||||||||
| An object passing this test has a __get__ attribute, but not a | ||||||||||
| __set__ attribute or a __delete__ attribute. Beyond that, the set | ||||||||||
| of attributes varies; __name__ is usually sensible, and __doc__ | ||||||||||
| often is. | ||||||||||
|
|
||||||||||
| Methods implemented via descriptors that also pass one of the other | ||||||||||
| tests return false from the ismethoddescriptor() test, simply because | ||||||||||
|
|
@@ -313,7 +314,9 @@ def ismethoddescriptor(object): | |||||||||
| # mutual exclusion | ||||||||||
| return False | ||||||||||
| tp = type(object) | ||||||||||
| return hasattr(tp, "__get__") and not hasattr(tp, "__set__") | ||||||||||
| if hasattr(tp, "__set__") or hasattr(tp, "__delete__"): | ||||||||||
| return False | ||||||||||
| return hasattr(tp, "__get__") | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would check
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, changed. :) |
||||||||||
|
|
||||||||||
| def isdatadescriptor(object): | ||||||||||
| """Return true if the object is a data descriptor. | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -53,9 +53,8 @@ | |||||||||||
| # ismodule, isclass, ismethod, isfunction, istraceback, isframe, iscode, | ||||||||||||
| # isbuiltin, isroutine, isgenerator, isgeneratorfunction, getmembers, | ||||||||||||
| # getdoc, getfile, getmodule, getsourcefile, getcomments, getsource, | ||||||||||||
| # getclasstree, getargvalues, formatargvalues, | ||||||||||||
| # currentframe, stack, trace, isdatadescriptor, | ||||||||||||
| # ismethodwrapper | ||||||||||||
| # getclasstree, getargvalues, formatargvalues, currentframe, | ||||||||||||
| # stack, trace, ismethoddescriptor, isdatadescriptor, ismethodwrapper | ||||||||||||
|
|
||||||||||||
| # NOTE: There are some additional tests relating to interaction with | ||||||||||||
| # zipimport in the test_zipimport_support test module. | ||||||||||||
|
|
@@ -177,6 +176,7 @@ def test_excluding_predicates(self): | |||||||||||
| self.istest(inspect.ismethod, 'git.argue') | ||||||||||||
| self.istest(inspect.ismethod, 'mod.custom_method') | ||||||||||||
| self.istest(inspect.ismodule, 'mod') | ||||||||||||
| self.istest(inspect.ismethoddescriptor, 'int.__add__') | ||||||||||||
| self.istest(inspect.isdatadescriptor, 'collections.defaultdict.default_factory') | ||||||||||||
| self.istest(inspect.isgenerator, '(x for x in range(2))') | ||||||||||||
| self.istest(inspect.isgeneratorfunction, 'generator_function_example') | ||||||||||||
|
|
@@ -1700,6 +1700,110 @@ def test_typing_replacement(self): | |||||||||||
| self.assertEqual(inspect.formatannotation(ann1), 'Union[List[testModule.typing.A], int]') | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class TestIsMethodDescriptor(unittest.TestCase): | ||||||||||||
|
|
||||||||||||
| def test_custom_descriptors(self): | ||||||||||||
| class MethodDescriptor: | ||||||||||||
| def __get__(self, *_): pass | ||||||||||||
| class MethodDescriptorSub(MethodDescriptor): | ||||||||||||
| pass | ||||||||||||
| class DataDescriptorWithNoGet: | ||||||||||||
| def __set__(self, *_): pass | ||||||||||||
| class DataDescriptorWithGetSet: | ||||||||||||
| def __get__(self, *_): pass | ||||||||||||
| def __set__(self, *_): pass | ||||||||||||
| class DataDescriptorWithGetDelete: | ||||||||||||
| def __get__(self, *_): pass | ||||||||||||
| def __delete__(self, *_): pass | ||||||||||||
| class DataDescriptorSub(DataDescriptorWithNoGet, | ||||||||||||
| DataDescriptorWithGetDelete, | ||||||||||||
| MethodDescriptor): | ||||||||||||
| pass | ||||||||||||
| self.assertTrue( | ||||||||||||
| inspect.ismethoddescriptor(MethodDescriptor()), | ||||||||||||
| '__get__ and no __set__/__delete__ => method descriptor') | ||||||||||||
| self.assertTrue( | ||||||||||||
| inspect.ismethoddescriptor(MethodDescriptorSub()), | ||||||||||||
| '__get__ (inherited) and no __set__/__delete__' | ||||||||||||
| ' => method descriptor') | ||||||||||||
| self.assertFalse( | ||||||||||||
| inspect.ismethoddescriptor(DataDescriptorWithNoGet()), | ||||||||||||
| '__set__ (and no __get__) => not a method descriptor') | ||||||||||||
| self.assertFalse( | ||||||||||||
| inspect.ismethoddescriptor(DataDescriptorWithGetSet()), | ||||||||||||
| '__get__ and __set__ => not a method descriptor') | ||||||||||||
| self.assertFalse( | ||||||||||||
| inspect.ismethoddescriptor(DataDescriptorWithGetDelete()), | ||||||||||||
| '__get__ and __delete__ => not a method descriptor') | ||||||||||||
| self.assertFalse( | ||||||||||||
| inspect.ismethoddescriptor(DataDescriptorSub()), | ||||||||||||
| '__get__, __set__ and __delete__ => not a method descriptor') | ||||||||||||
| # Note: descriptor classes themselves are *not* descriptors. | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(MethodDescriptor)) | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(MethodDescriptorSub)) | ||||||||||||
|
|
||||||||||||
| def test_builtin_descriptors(self): | ||||||||||||
| builtin_slot_wrapper = int.__add__ # This one is mentioned in docs. | ||||||||||||
| class Owner: | ||||||||||||
| def instance_method(self): pass | ||||||||||||
| @classmethod | ||||||||||||
| def class_method(cls): pass | ||||||||||||
| @staticmethod | ||||||||||||
| def static_method(): pass | ||||||||||||
| @property | ||||||||||||
| def a_property(self): pass | ||||||||||||
| def function(): | ||||||||||||
| pass | ||||||||||||
| a_lambda = lambda: None | ||||||||||||
| class Slotted: | ||||||||||||
| __slots__ = 'foo', | ||||||||||||
| # Example builtin method descriptors: | ||||||||||||
| self.assertTrue( | ||||||||||||
| inspect.ismethoddescriptor(builtin_slot_wrapper), | ||||||||||||
| 'a builtin slot wrapper is a method descriptor') | ||||||||||||
| self.assertTrue( | ||||||||||||
| inspect.ismethoddescriptor(Owner.__dict__['class_method']), | ||||||||||||
| 'a classmethod object is a method descriptor') | ||||||||||||
| self.assertTrue( | ||||||||||||
| inspect.ismethoddescriptor(Owner.__dict__['static_method']), | ||||||||||||
| 'a staticmethod object is a method descriptor') | ||||||||||||
| # Example builtin data descriptors: | ||||||||||||
| self.assertFalse( | ||||||||||||
| inspect.ismethoddescriptor(Slotted.foo), | ||||||||||||
| 'a slot is not a method descriptor') | ||||||||||||
| self.assertFalse( | ||||||||||||
| inspect.ismethoddescriptor(Owner.a_property), | ||||||||||||
| 'a property is not a method descriptor') | ||||||||||||
| # `types.MethodType`/`types.FunctionType` instances (they *are* | ||||||||||||
| # method descriptors, but `ismethoddescriptor()` explicitly | ||||||||||||
| # excludes them): | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(Owner().instance_method)) | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(Owner().class_method)) | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(Owner().static_method)) | ||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Frankly, I'd rather prefer to reduce the number of these cases... I'd keep:
I'd remove: When it comes to the ones you proposed, please note that This test class should focus on the behavior of (In the first version, I wanted to mimic some elements of the already existing test class
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you agree with my reasoning, or would you prefer these new cases to be added anyway? Could you please decide, I have no strong feelings about this particular detail. :)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer having all possible cases. One reason is that it shows how robust the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, added :)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you! (it also implicitly helps in asserting that a function and a lambda function have the same type) |
||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(function)) | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(a_lambda)) | ||||||||||||
|
|
||||||||||||
| def test_descriptor_being_a_class(self): | ||||||||||||
| class MethodDescriptorMeta(type): | ||||||||||||
| def __get__(self, *_): pass | ||||||||||||
| class ClassBeingMethodDescriptor(metaclass=MethodDescriptorMeta): | ||||||||||||
| pass | ||||||||||||
| # `ClassBeingMethodDescriptor` itself *is* a method descriptor, | ||||||||||||
| # but it is *also* a class, and `ismethoddescriptor()` explicitly | ||||||||||||
| # excludes classes. | ||||||||||||
| self.assertFalse( | ||||||||||||
| inspect.ismethoddescriptor(ClassBeingMethodDescriptor), | ||||||||||||
| 'classes (instances of type) are explicitly excluded') | ||||||||||||
|
|
||||||||||||
| def test_non_descriptors(self): | ||||||||||||
| class Test: | ||||||||||||
| pass | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(Test())) | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(Test)) | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor([42])) | ||||||||||||
| self.assertFalse(inspect.ismethoddescriptor(42)) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class TestIsDataDescriptor(unittest.TestCase): | ||||||||||||
|
|
||||||||||||
| def test_custom_descriptors(self): | ||||||||||||
|
|
||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Correct :func:`inspect.ismethoddescriptor` to check also for the lack of | ||
| :meth:`~object.__delete__`. Patch by Jan Kaliszewski. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this part of the text requires a
.. versionadded:: 3.14, but I am not sure on thatUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe
.. versionchanged:: .....would be more approproate, as here we change the behavior of an existing entity, rather than add a new entity.Also – here we come back to the question I asked at the beginning – shouldn't it be applied also to Python 3.13? (considering that Python 3.13 is still in the beta phase, and here we have a bugfix, rather than a feature, even if it seems too disruptive to be backported to the 3.12 and earlier Python versions...)
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes,
versionchangedwould be the right docs notation.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The note in the
__set__documentation (along with the interpreter's actual behaviour) makes it clear that this is a bugfix (i.e. adding__delete__is enough to make a descriptor a data descriptor), so backporting to 3.13 makes sense to me.I also agree that the risk/reward for backporting further isn't there, so 3.13 is as far back as the change should go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, added. :)