bpo-38364: unwrap partialmethods just like we unwrap partials (#16600)

* bpo-38364: unwrap partialmethods just like we unwrap partials

The inspect.isgeneratorfunction, inspect.iscoroutinefunction and inspect.isasyncgenfunction already unwrap functools.partial objects, this patch adds support for partialmethod objects as well.

Also: Rename _partialmethod to __partialmethod__.
Since we're checking this attribute on arbitrary function-like objects,
we should use the namespace reserved for core Python.

---------

Co-authored-by: Petr Viktorin <encukou@gmail.com>
This commit is contained in:
Martijn Pieters 2024-02-15 11:08:45 +00:00 committed by GitHub
parent 9e3729bbd7
commit edb59d5718
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 57 additions and 4 deletions

View File

@ -340,6 +340,9 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
Functions wrapped in :func:`functools.partial` now return ``True`` if the Functions wrapped in :func:`functools.partial` now return ``True`` if the
wrapped function is a Python generator function. wrapped function is a Python generator function.
.. versionchanged:: 3.13
Functions wrapped in :func:`functools.partialmethod` now return ``True``
if the wrapped function is a Python generator function.
.. function:: isgenerator(object) .. function:: isgenerator(object)
@ -363,6 +366,10 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
Sync functions marked with :func:`markcoroutinefunction` now return Sync functions marked with :func:`markcoroutinefunction` now return
``True``. ``True``.
.. versionchanged:: 3.13
Functions wrapped in :func:`functools.partialmethod` now return ``True``
if the wrapped function is a :term:`coroutine function`.
.. function:: markcoroutinefunction(func) .. function:: markcoroutinefunction(func)
@ -429,6 +436,9 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
Functions wrapped in :func:`functools.partial` now return ``True`` if the Functions wrapped in :func:`functools.partial` now return ``True`` if the
wrapped function is a :term:`asynchronous generator` function. wrapped function is a :term:`asynchronous generator` function.
.. versionchanged:: 3.13
Functions wrapped in :func:`functools.partialmethod` now return ``True``
if the wrapped function is a :term:`coroutine function`.
.. function:: isasyncgen(object) .. function:: isasyncgen(object)

View File

@ -388,7 +388,7 @@ class partialmethod(object):
keywords = {**self.keywords, **keywords} keywords = {**self.keywords, **keywords}
return self.func(cls_or_self, *self.args, *args, **keywords) return self.func(cls_or_self, *self.args, *args, **keywords)
_method.__isabstractmethod__ = self.__isabstractmethod__ _method.__isabstractmethod__ = self.__isabstractmethod__
_method._partialmethod = self _method.__partialmethod__ = self
return _method return _method
def __get__(self, obj, cls=None): def __get__(self, obj, cls=None):
@ -424,6 +424,17 @@ def _unwrap_partial(func):
func = func.func func = func.func
return func return func
def _unwrap_partialmethod(func):
prev = None
while func is not prev:
prev = func
while isinstance(getattr(func, "__partialmethod__", None), partialmethod):
func = func.__partialmethod__
while isinstance(func, partialmethod):
func = getattr(func, 'func')
func = _unwrap_partial(func)
return func
################################################################################ ################################################################################
### LRU Cache function decorator ### LRU Cache function decorator
################################################################################ ################################################################################

View File

@ -383,8 +383,10 @@ def isfunction(object):
def _has_code_flag(f, flag): def _has_code_flag(f, flag):
"""Return true if ``f`` is a function (or a method or functools.partial """Return true if ``f`` is a function (or a method or functools.partial
wrapper wrapping a function) whose code object has the given ``flag`` wrapper wrapping a function or a functools.partialmethod wrapping a
function) whose code object has the given ``flag``
set in its flags.""" set in its flags."""
f = functools._unwrap_partialmethod(f)
while ismethod(f): while ismethod(f):
f = f.__func__ f = f.__func__
f = functools._unwrap_partial(f) f = functools._unwrap_partial(f)
@ -2561,7 +2563,7 @@ def _signature_from_callable(obj, *,
return sig return sig
try: try:
partialmethod = obj._partialmethod partialmethod = obj.__partialmethod__
except AttributeError: except AttributeError:
pass pass
else: else:

View File

@ -206,12 +206,33 @@ class TestPredicates(IsTestBase):
gen_coro = gen_coroutine_function_example(1) gen_coro = gen_coroutine_function_example(1)
coro = coroutine_function_example(1) coro = coroutine_function_example(1)
class PMClass:
async_generator_partialmethod_example = functools.partialmethod(
async_generator_function_example)
coroutine_partialmethod_example = functools.partialmethod(
coroutine_function_example)
gen_coroutine_partialmethod_example = functools.partialmethod(
gen_coroutine_function_example)
# partialmethods on the class, bound to an instance
pm_instance = PMClass()
async_gen_coro_pmi = pm_instance.async_generator_partialmethod_example
gen_coro_pmi = pm_instance.gen_coroutine_partialmethod_example
coro_pmi = pm_instance.coroutine_partialmethod_example
# partialmethods on the class, unbound but accessed via the class
async_gen_coro_pmc = PMClass.async_generator_partialmethod_example
gen_coro_pmc = PMClass.gen_coroutine_partialmethod_example
coro_pmc = PMClass.coroutine_partialmethod_example
self.assertFalse( self.assertFalse(
inspect.iscoroutinefunction(gen_coroutine_function_example)) inspect.iscoroutinefunction(gen_coroutine_function_example))
self.assertFalse( self.assertFalse(
inspect.iscoroutinefunction( inspect.iscoroutinefunction(
functools.partial(functools.partial( functools.partial(functools.partial(
gen_coroutine_function_example)))) gen_coroutine_function_example))))
self.assertFalse(inspect.iscoroutinefunction(gen_coro_pmi))
self.assertFalse(inspect.iscoroutinefunction(gen_coro_pmc))
self.assertFalse(inspect.iscoroutine(gen_coro)) self.assertFalse(inspect.iscoroutine(gen_coro))
self.assertTrue( self.assertTrue(
@ -220,6 +241,8 @@ class TestPredicates(IsTestBase):
inspect.isgeneratorfunction( inspect.isgeneratorfunction(
functools.partial(functools.partial( functools.partial(functools.partial(
gen_coroutine_function_example)))) gen_coroutine_function_example))))
self.assertTrue(inspect.isgeneratorfunction(gen_coro_pmi))
self.assertTrue(inspect.isgeneratorfunction(gen_coro_pmc))
self.assertTrue(inspect.isgenerator(gen_coro)) self.assertTrue(inspect.isgenerator(gen_coro))
async def _fn3(): async def _fn3():
@ -285,6 +308,8 @@ class TestPredicates(IsTestBase):
inspect.iscoroutinefunction( inspect.iscoroutinefunction(
functools.partial(functools.partial( functools.partial(functools.partial(
coroutine_function_example)))) coroutine_function_example))))
self.assertTrue(inspect.iscoroutinefunction(coro_pmi))
self.assertTrue(inspect.iscoroutinefunction(coro_pmc))
self.assertTrue(inspect.iscoroutine(coro)) self.assertTrue(inspect.iscoroutine(coro))
self.assertFalse( self.assertFalse(
@ -297,6 +322,8 @@ class TestPredicates(IsTestBase):
inspect.isgeneratorfunction( inspect.isgeneratorfunction(
functools.partial(functools.partial( functools.partial(functools.partial(
coroutine_function_example)))) coroutine_function_example))))
self.assertFalse(inspect.isgeneratorfunction(coro_pmi))
self.assertFalse(inspect.isgeneratorfunction(coro_pmc))
self.assertFalse(inspect.isgenerator(coro)) self.assertFalse(inspect.isgenerator(coro))
self.assertFalse( self.assertFalse(
@ -311,6 +338,8 @@ class TestPredicates(IsTestBase):
inspect.isasyncgenfunction( inspect.isasyncgenfunction(
functools.partial(functools.partial( functools.partial(functools.partial(
async_generator_function_example)))) async_generator_function_example))))
self.assertTrue(inspect.isasyncgenfunction(async_gen_coro_pmi))
self.assertTrue(inspect.isasyncgenfunction(async_gen_coro_pmc))
self.assertTrue(inspect.isasyncgen(async_gen_coro)) self.assertTrue(inspect.isasyncgen(async_gen_coro))
coro.close(); gen_coro.close(); # silence warnings coro.close(); gen_coro.close(); # silence warnings
@ -3389,7 +3418,7 @@ class TestSignatureObject(unittest.TestCase):
def test_signature_on_fake_partialmethod(self): def test_signature_on_fake_partialmethod(self):
def foo(a): pass def foo(a): pass
foo._partialmethod = 'spam' foo.__partialmethod__ = 'spam'
self.assertEqual(str(inspect.signature(foo)), '(a)') self.assertEqual(str(inspect.signature(foo)), '(a)')
def test_signature_on_decorated(self): def test_signature_on_decorated(self):

View File

@ -0,0 +1 @@
The ``inspect`` functions ``isgeneratorfunction``, ``iscoroutinefunction``, ``isasyncgenfunction`` now support ``functools.partialmethod`` wrapped functions the same way they support ``functools.partial``.