Skip to content

Commit edb59d5

Browse files
mjpietersencukou
andauthored
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>
1 parent 9e3729b commit edb59d5

File tree

5 files changed

+57
-4
lines changed

5 files changed

+57
-4
lines changed

Doc/library/inspect.rst

+10
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
340340
Functions wrapped in :func:`functools.partial` now return ``True`` if the
341341
wrapped function is a Python generator function.
342342

343+
.. versionchanged:: 3.13
344+
Functions wrapped in :func:`functools.partialmethod` now return ``True``
345+
if the wrapped function is a Python generator function.
343346

344347
.. function:: isgenerator(object)
345348

@@ -363,6 +366,10 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
363366
Sync functions marked with :func:`markcoroutinefunction` now return
364367
``True``.
365368

369+
.. versionchanged:: 3.13
370+
Functions wrapped in :func:`functools.partialmethod` now return ``True``
371+
if the wrapped function is a :term:`coroutine function`.
372+
366373

367374
.. function:: markcoroutinefunction(func)
368375

@@ -429,6 +436,9 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
429436
Functions wrapped in :func:`functools.partial` now return ``True`` if the
430437
wrapped function is a :term:`asynchronous generator` function.
431438

439+
.. versionchanged:: 3.13
440+
Functions wrapped in :func:`functools.partialmethod` now return ``True``
441+
if the wrapped function is a :term:`coroutine function`.
432442

433443
.. function:: isasyncgen(object)
434444

Lib/functools.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ def _method(cls_or_self, /, *args, **keywords):
388388
keywords = {**self.keywords, **keywords}
389389
return self.func(cls_or_self, *self.args, *args, **keywords)
390390
_method.__isabstractmethod__ = self.__isabstractmethod__
391-
_method._partialmethod = self
391+
_method.__partialmethod__ = self
392392
return _method
393393

394394
def __get__(self, obj, cls=None):
@@ -424,6 +424,17 @@ def _unwrap_partial(func):
424424
func = func.func
425425
return func
426426

427+
def _unwrap_partialmethod(func):
428+
prev = None
429+
while func is not prev:
430+
prev = func
431+
while isinstance(getattr(func, "__partialmethod__", None), partialmethod):
432+
func = func.__partialmethod__
433+
while isinstance(func, partialmethod):
434+
func = getattr(func, 'func')
435+
func = _unwrap_partial(func)
436+
return func
437+
427438
################################################################################
428439
### LRU Cache function decorator
429440
################################################################################

Lib/inspect.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -383,8 +383,10 @@ def isfunction(object):
383383

384384
def _has_code_flag(f, flag):
385385
"""Return true if ``f`` is a function (or a method or functools.partial
386-
wrapper wrapping a function) whose code object has the given ``flag``
386+
wrapper wrapping a function or a functools.partialmethod wrapping a
387+
function) whose code object has the given ``flag``
387388
set in its flags."""
389+
f = functools._unwrap_partialmethod(f)
388390
while ismethod(f):
389391
f = f.__func__
390392
f = functools._unwrap_partial(f)
@@ -2561,7 +2563,7 @@ def _signature_from_callable(obj, *,
25612563
return sig
25622564

25632565
try:
2564-
partialmethod = obj._partialmethod
2566+
partialmethod = obj.__partialmethod__
25652567
except AttributeError:
25662568
pass
25672569
else:

Lib/test/test_inspect/test_inspect.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,33 @@ def test_iscoroutine(self):
206206
gen_coro = gen_coroutine_function_example(1)
207207
coro = coroutine_function_example(1)
208208

209+
class PMClass:
210+
async_generator_partialmethod_example = functools.partialmethod(
211+
async_generator_function_example)
212+
coroutine_partialmethod_example = functools.partialmethod(
213+
coroutine_function_example)
214+
gen_coroutine_partialmethod_example = functools.partialmethod(
215+
gen_coroutine_function_example)
216+
217+
# partialmethods on the class, bound to an instance
218+
pm_instance = PMClass()
219+
async_gen_coro_pmi = pm_instance.async_generator_partialmethod_example
220+
gen_coro_pmi = pm_instance.gen_coroutine_partialmethod_example
221+
coro_pmi = pm_instance.coroutine_partialmethod_example
222+
223+
# partialmethods on the class, unbound but accessed via the class
224+
async_gen_coro_pmc = PMClass.async_generator_partialmethod_example
225+
gen_coro_pmc = PMClass.gen_coroutine_partialmethod_example
226+
coro_pmc = PMClass.coroutine_partialmethod_example
227+
209228
self.assertFalse(
210229
inspect.iscoroutinefunction(gen_coroutine_function_example))
211230
self.assertFalse(
212231
inspect.iscoroutinefunction(
213232
functools.partial(functools.partial(
214233
gen_coroutine_function_example))))
234+
self.assertFalse(inspect.iscoroutinefunction(gen_coro_pmi))
235+
self.assertFalse(inspect.iscoroutinefunction(gen_coro_pmc))
215236
self.assertFalse(inspect.iscoroutine(gen_coro))
216237

217238
self.assertTrue(
@@ -220,6 +241,8 @@ def test_iscoroutine(self):
220241
inspect.isgeneratorfunction(
221242
functools.partial(functools.partial(
222243
gen_coroutine_function_example))))
244+
self.assertTrue(inspect.isgeneratorfunction(gen_coro_pmi))
245+
self.assertTrue(inspect.isgeneratorfunction(gen_coro_pmc))
223246
self.assertTrue(inspect.isgenerator(gen_coro))
224247

225248
async def _fn3():
@@ -285,6 +308,8 @@ def do_something_static():
285308
inspect.iscoroutinefunction(
286309
functools.partial(functools.partial(
287310
coroutine_function_example))))
311+
self.assertTrue(inspect.iscoroutinefunction(coro_pmi))
312+
self.assertTrue(inspect.iscoroutinefunction(coro_pmc))
288313
self.assertTrue(inspect.iscoroutine(coro))
289314

290315
self.assertFalse(
@@ -297,6 +322,8 @@ def do_something_static():
297322
inspect.isgeneratorfunction(
298323
functools.partial(functools.partial(
299324
coroutine_function_example))))
325+
self.assertFalse(inspect.isgeneratorfunction(coro_pmi))
326+
self.assertFalse(inspect.isgeneratorfunction(coro_pmc))
300327
self.assertFalse(inspect.isgenerator(coro))
301328

302329
self.assertFalse(
@@ -311,6 +338,8 @@ def do_something_static():
311338
inspect.isasyncgenfunction(
312339
functools.partial(functools.partial(
313340
async_generator_function_example))))
341+
self.assertTrue(inspect.isasyncgenfunction(async_gen_coro_pmi))
342+
self.assertTrue(inspect.isasyncgenfunction(async_gen_coro_pmc))
314343
self.assertTrue(inspect.isasyncgen(async_gen_coro))
315344

316345
coro.close(); gen_coro.close(); # silence warnings
@@ -3389,7 +3418,7 @@ def test(self: 'anno', x):
33893418

33903419
def test_signature_on_fake_partialmethod(self):
33913420
def foo(a): pass
3392-
foo._partialmethod = 'spam'
3421+
foo.__partialmethod__ = 'spam'
33933422
self.assertEqual(str(inspect.signature(foo)), '(a)')
33943423

33953424
def test_signature_on_decorated(self):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The ``inspect`` functions ``isgeneratorfunction``, ``iscoroutinefunction``, ``isasyncgenfunction`` now support ``functools.partialmethod`` wrapped functions the same way they support ``functools.partial``.

0 commit comments

Comments
 (0)