Skip to content

Commit 9cb267a

Browse files
committed
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.
1 parent 98308db commit 9cb267a

File tree

4 files changed

+53
-1
lines changed

4 files changed

+53
-1
lines changed

Doc/library/inspect.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,9 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
335335
Functions wrapped in :func:`functools.partial` now return ``True`` if the
336336
wrapped function is a Python generator function.
337337

338+
.. versionchanged:: 3.12
339+
Functions wrapped in :func:`functools.partialmethod` now return ``True``
340+
if the wrapped function is a Python generator function.
338341

339342
.. function:: isgenerator(object)
340343

@@ -354,6 +357,10 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
354357
Functions wrapped in :func:`functools.partial` now return ``True`` if the
355358
wrapped function is a :term:`coroutine function`.
356359

360+
.. versionchanged:: 3.12
361+
Functions wrapped in :func:`functools.partialmethod` now return ``True``
362+
if the wrapped function is a :term:`coroutine function`.
363+
357364
.. versionchanged:: 3.12
358365
Sync functions marked with :func:`markcoroutinefunction` now return
359366
``True``.
@@ -418,6 +425,9 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
418425
Functions wrapped in :func:`functools.partial` now return ``True`` if the
419426
wrapped function is a :term:`asynchronous generator` function.
420427

428+
.. versionchanged:: 3.12
429+
Functions wrapped in :func:`functools.partialmethod` now return ``True``
430+
if the wrapped function is a :term:`coroutine function`.
421431

422432
.. function:: isasyncgen(object)
423433

Lib/functools.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,17 @@ def _unwrap_partial(func):
423423
func = func.func
424424
return func
425425

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

Lib/inspect.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,10 @@ def isfunction(object):
376376

377377
def _has_code_flag(f, flag):
378378
"""Return true if ``f`` is a function (or a method or functools.partial
379-
wrapper wrapping a function) whose code object has the given ``flag``
379+
wrapper wrapping a function or a functools.partialmethod wrapping a
380+
function) whose code object has the given ``flag``
380381
set in its flags."""
382+
f = functools._unwrap_partialmethod(f)
381383
while ismethod(f):
382384
f = f.__func__
383385
f = functools._unwrap_partial(f)

Lib/test/test_inspect.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,33 @@ def test_iscoroutine(self):
186186
gen_coro = gen_coroutine_function_example(1)
187187
coro = coroutine_function_example(1)
188188

189+
class PMClass:
190+
async_generator_partialmethod_example = functools.partialmethod(
191+
async_generator_function_example)
192+
coroutine_partialmethod_example = functools.partialmethod(
193+
coroutine_function_example)
194+
gen_coroutine_partialmethod_example = functools.partialmethod(
195+
gen_coroutine_function_example)
196+
197+
# partialmethods on the class, bound to an instance
198+
pm_instance = PMClass()
199+
async_gen_coro_pmi = pm_instance.async_generator_partialmethod_example
200+
gen_coro_pmi = pm_instance.gen_coroutine_partialmethod_example
201+
coro_pmi = pm_instance.coroutine_partialmethod_example
202+
203+
# partialmethods on the class, unbound but accessed via the class
204+
async_gen_coro_pmc = PMClass.async_generator_partialmethod_example
205+
gen_coro_pmc = PMClass.gen_coroutine_partialmethod_example
206+
coro_pmc = PMClass.coroutine_partialmethod_example
207+
189208
self.assertFalse(
190209
inspect.iscoroutinefunction(gen_coroutine_function_example))
191210
self.assertFalse(
192211
inspect.iscoroutinefunction(
193212
functools.partial(functools.partial(
194213
gen_coroutine_function_example))))
214+
self.assertFalse(inspect.iscoroutinefunction(gen_coro_pmi))
215+
self.assertFalse(inspect.iscoroutinefunction(gen_coro_pmc))
195216
self.assertFalse(inspect.iscoroutine(gen_coro))
196217

197218
self.assertTrue(
@@ -200,6 +221,8 @@ def test_iscoroutine(self):
200221
inspect.isgeneratorfunction(
201222
functools.partial(functools.partial(
202223
gen_coroutine_function_example))))
224+
self.assertTrue(inspect.isgeneratorfunction(gen_coro_pmi))
225+
self.assertTrue(inspect.isgeneratorfunction(gen_coro_pmc))
203226
self.assertTrue(inspect.isgenerator(gen_coro))
204227

205228
async def _fn3():
@@ -257,6 +280,8 @@ def do_something_static():
257280
inspect.iscoroutinefunction(
258281
functools.partial(functools.partial(
259282
coroutine_function_example))))
283+
self.assertTrue(inspect.iscoroutinefunction(coro_pmi))
284+
self.assertTrue(inspect.iscoroutinefunction(coro_pmc))
260285
self.assertTrue(inspect.iscoroutine(coro))
261286

262287
self.assertFalse(
@@ -269,6 +294,8 @@ def do_something_static():
269294
inspect.isgeneratorfunction(
270295
functools.partial(functools.partial(
271296
coroutine_function_example))))
297+
self.assertFalse(inspect.isgeneratorfunction(coro_pmi))
298+
self.assertFalse(inspect.isgeneratorfunction(coro_pmc))
272299
self.assertFalse(inspect.isgenerator(coro))
273300

274301
self.assertFalse(
@@ -283,6 +310,8 @@ def do_something_static():
283310
inspect.isasyncgenfunction(
284311
functools.partial(functools.partial(
285312
async_generator_function_example))))
313+
self.assertTrue(inspect.isasyncgenfunction(async_gen_coro_pmi))
314+
self.assertTrue(inspect.isasyncgenfunction(async_gen_coro_pmc))
286315
self.assertTrue(inspect.isasyncgen(async_gen_coro))
287316

288317
coro.close(); gen_coro.close(); # silence warnings

0 commit comments

Comments
 (0)