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 edb59d5

Browse filesBrowse 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
Copy full SHA for edb59d5

File tree

5 files changed

+57
-4
lines changed
Filter options

5 files changed

+57
-4
lines changed

‎Doc/library/inspect.rst

Copy file name to clipboardExpand all lines: Doc/library/inspect.rst
+10Lines changed: 10 additions & 0 deletions
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

Copy file name to clipboardExpand all lines: Lib/functools.py
+12-1Lines changed: 12 additions & 1 deletion
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

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

Copy file name to clipboardExpand all lines: Lib/test/test_inspect/test_inspect.py
+30-1Lines changed: 30 additions & 1 deletion
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):
+1Lines changed: 1 addition & 0 deletions
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.