Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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.
  • Loading branch information
mjpieters committed Dec 31, 2022
commit 9cb267acae2bc915659711e39c1cdf188448390b
10 changes: 10 additions & 0 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
Functions wrapped in :func:`functools.partial` now return ``True`` if the
wrapped function is a Python generator function.

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

.. function:: isgenerator(object)

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

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

.. versionchanged:: 3.12
Sync functions marked with :func:`markcoroutinefunction` now return
``True``.
Expand Down Expand Up @@ -418,6 +425,9 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
Functions wrapped in :func:`functools.partial` now return ``True`` if the
wrapped function is a :term:`asynchronous generator` function.

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

.. function:: isasyncgen(object)

Expand Down
11 changes: 11 additions & 0 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,17 @@ def _unwrap_partial(func):
func = func.func
return func

def _unwrap_partialmethod(func):
prev = None
while func is not prev:
prev = func
while isinstance(getattr(func, "_partialmethod", None), partialmethod):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why getattr? What objects have _partialmethod?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

partialmethod(...).__get__() can return a closure with _partialmethod pointing to the instance. This happens if the wrapped callable is not a descriptor, or if the descriptor returned itself again.

See the partialmethod._make_unbound_method() implementation.

getattr() is used because the attribute is optional, and you'd not want this to break even if the supplied object does have a _partialmethod attribute that is not actually a partialmethod instance.

func = func._partialmethod
while isinstance(func, partialmethod):
func = getattr(func, 'func')
func = _unwrap_partial(func)
return func

################################################################################
### LRU Cache function decorator
################################################################################
Expand Down
4 changes: 3 additions & 1 deletion Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,10 @@ def isfunction(object):

def _has_code_flag(f, flag):
"""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``
Comment thread
mjpieters marked this conversation as resolved.
set in its flags."""
f = functools._unwrap_partialmethod(f)
while ismethod(f):
f = f.__func__
f = functools._unwrap_partial(f)
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,33 @@ def test_iscoroutine(self):
gen_coro = gen_coroutine_function_example(1)
coro = coroutine_function_example(1)

class PMClass:
Comment thread
mjpieters marked this conversation as resolved.
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(
inspect.iscoroutinefunction(gen_coroutine_function_example))
self.assertFalse(
inspect.iscoroutinefunction(
functools.partial(functools.partial(
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.assertTrue(
Expand All @@ -200,6 +221,8 @@ def test_iscoroutine(self):
inspect.isgeneratorfunction(
functools.partial(functools.partial(
gen_coroutine_function_example))))
self.assertTrue(inspect.isgeneratorfunction(gen_coro_pmi))
self.assertTrue(inspect.isgeneratorfunction(gen_coro_pmc))
self.assertTrue(inspect.isgenerator(gen_coro))

async def _fn3():
Expand Down Expand Up @@ -257,6 +280,8 @@ def do_something_static():
inspect.iscoroutinefunction(
functools.partial(functools.partial(
coroutine_function_example))))
self.assertTrue(inspect.iscoroutinefunction(coro_pmi))
self.assertTrue(inspect.iscoroutinefunction(coro_pmc))
self.assertTrue(inspect.iscoroutine(coro))

self.assertFalse(
Expand All @@ -269,6 +294,8 @@ def do_something_static():
inspect.isgeneratorfunction(
functools.partial(functools.partial(
coroutine_function_example))))
self.assertFalse(inspect.isgeneratorfunction(coro_pmi))
self.assertFalse(inspect.isgeneratorfunction(coro_pmc))
self.assertFalse(inspect.isgenerator(coro))

self.assertFalse(
Expand All @@ -283,6 +310,8 @@ def do_something_static():
inspect.isasyncgenfunction(
functools.partial(functools.partial(
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))

coro.close(); gen_coro.close(); # silence warnings
Expand Down