bpo-34890: Make iscoroutinefunction, isgeneratorfunction and isasyncgenfunction work with functools.partial (GH-9903)

inspect.isfunction() processes both inspect.isfunction(func) and
inspect.isfunction(partial(func, arg)) correctly but some other functions in the
inspect module (iscoroutinefunction, isgeneratorfunction and isasyncgenfunction)
lack this functionality. This commits adds a new check in the mentioned functions
in the inspect module so they can work correctly with arbitrarily nested partial
functions.
This commit is contained in:
Pablo Galindo 2018-10-26 12:19:14 +01:00 committed by GitHub
parent e483f02423
commit 7cd2543416
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 61 additions and 12 deletions

View File

@ -298,6 +298,10 @@ attributes:
Return true if the object is a Python generator function. Return true if the object is a Python generator function.
.. versionchanged:: 3.8
Functions wrapped in :func:`functools.partial` now return true if the
wrapped function is a Python generator function.
.. function:: isgenerator(object) .. function:: isgenerator(object)
@ -311,6 +315,10 @@ attributes:
.. versionadded:: 3.5 .. versionadded:: 3.5
.. versionchanged:: 3.8
Functions wrapped in :func:`functools.partial` now return true if the
wrapped function is a :term:`coroutine function`.
.. function:: iscoroutine(object) .. function:: iscoroutine(object)
@ -352,6 +360,10 @@ attributes:
.. versionadded:: 3.6 .. versionadded:: 3.6
.. versionchanged:: 3.8
Functions wrapped in :func:`functools.partial` now return true if the
wrapped function is a :term:`asynchronous generator` function.
.. function:: isasyncgen(object) .. function:: isasyncgen(object)

View File

@ -423,6 +423,12 @@ class partialmethod(object):
def __isabstractmethod__(self): def __isabstractmethod__(self):
return getattr(self.func, "__isabstractmethod__", False) return getattr(self.func, "__isabstractmethod__", False)
# Helper functions
def _unwrap_partial(func):
while isinstance(func, partial):
func = func.func
return func
################################################################################ ################################################################################
### LRU Cache function decorator ### LRU Cache function decorator

View File

@ -168,30 +168,33 @@ def isfunction(object):
__kwdefaults__ dict of keyword only parameters with defaults""" __kwdefaults__ dict of keyword only parameters with defaults"""
return isinstance(object, types.FunctionType) return isinstance(object, types.FunctionType)
def isgeneratorfunction(object): def isgeneratorfunction(obj):
"""Return true if the object is a user-defined generator function. """Return true if the object is a user-defined generator function.
Generator function objects provide the same attributes as functions. Generator function objects provide the same attributes as functions.
See help(isfunction) for a list of attributes.""" See help(isfunction) for a list of attributes."""
return bool((isfunction(object) or ismethod(object)) and obj = functools._unwrap_partial(obj)
object.__code__.co_flags & CO_GENERATOR) return bool((isfunction(obj) or ismethod(obj)) and
obj.__code__.co_flags & CO_GENERATOR)
def iscoroutinefunction(object): def iscoroutinefunction(obj):
"""Return true if the object is a coroutine function. """Return true if the object is a coroutine function.
Coroutine functions are defined with "async def" syntax. Coroutine functions are defined with "async def" syntax.
""" """
return bool((isfunction(object) or ismethod(object)) and obj = functools._unwrap_partial(obj)
object.__code__.co_flags & CO_COROUTINE) return bool(((isfunction(obj) or ismethod(obj)) and
obj.__code__.co_flags & CO_COROUTINE))
def isasyncgenfunction(object): def isasyncgenfunction(obj):
"""Return true if the object is an asynchronous generator function. """Return true if the object is an asynchronous generator function.
Asynchronous generator functions are defined with "async def" Asynchronous generator functions are defined with "async def"
syntax and have "yield" expressions in their body. syntax and have "yield" expressions in their body.
""" """
return bool((isfunction(object) or ismethod(object)) and obj = functools._unwrap_partial(obj)
object.__code__.co_flags & CO_ASYNC_GENERATOR) return bool((isfunction(obj) or ismethod(obj)) and
obj.__code__.co_flags & CO_ASYNC_GENERATOR)
def isasyncgen(object): def isasyncgen(object):
"""Return true if the object is an asynchronous generator.""" """Return true if the object is an asynchronous generator."""

View File

@ -440,8 +440,8 @@ class BaseTaskTests:
coro_repr = repr(task._coro) coro_repr = repr(task._coro)
expected = ( expected = (
r'<CoroWrapper \w+.test_task_repr_partial_corowrapper' r'<coroutine object \w+\.test_task_repr_partial_corowrapper'
r'\.<locals>\.func\(1\)\(\) running, ' r'\.<locals>\.func at'
) )
self.assertRegex(coro_repr, expected) self.assertRegex(coro_repr, expected)

View File

@ -166,26 +166,51 @@ class TestPredicates(IsTestBase):
self.assertFalse(inspect.ismemberdescriptor(datetime.timedelta.days)) self.assertFalse(inspect.ismemberdescriptor(datetime.timedelta.days))
def test_iscoroutine(self): def test_iscoroutine(self):
async_gen_coro = async_generator_function_example(1)
gen_coro = gen_coroutine_function_example(1) gen_coro = gen_coroutine_function_example(1)
coro = coroutine_function_example(1) coro = coroutine_function_example(1)
self.assertFalse( self.assertFalse(
inspect.iscoroutinefunction(gen_coroutine_function_example)) inspect.iscoroutinefunction(gen_coroutine_function_example))
self.assertFalse(
inspect.iscoroutinefunction(
functools.partial(functools.partial(
gen_coroutine_function_example))))
self.assertFalse(inspect.iscoroutine(gen_coro)) self.assertFalse(inspect.iscoroutine(gen_coro))
self.assertTrue( self.assertTrue(
inspect.isgeneratorfunction(gen_coroutine_function_example)) inspect.isgeneratorfunction(gen_coroutine_function_example))
self.assertTrue(
inspect.isgeneratorfunction(
functools.partial(functools.partial(
gen_coroutine_function_example))))
self.assertTrue(inspect.isgenerator(gen_coro)) self.assertTrue(inspect.isgenerator(gen_coro))
self.assertTrue( self.assertTrue(
inspect.iscoroutinefunction(coroutine_function_example)) inspect.iscoroutinefunction(coroutine_function_example))
self.assertTrue(
inspect.iscoroutinefunction(
functools.partial(functools.partial(
coroutine_function_example))))
self.assertTrue(inspect.iscoroutine(coro)) self.assertTrue(inspect.iscoroutine(coro))
self.assertFalse( self.assertFalse(
inspect.isgeneratorfunction(coroutine_function_example)) inspect.isgeneratorfunction(coroutine_function_example))
self.assertFalse(
inspect.isgeneratorfunction(
functools.partial(functools.partial(
coroutine_function_example))))
self.assertFalse(inspect.isgenerator(coro)) self.assertFalse(inspect.isgenerator(coro))
coro.close(); gen_coro.close() # silence warnings self.assertTrue(
inspect.isasyncgenfunction(async_generator_function_example))
self.assertTrue(
inspect.isasyncgenfunction(
functools.partial(functools.partial(
async_generator_function_example))))
self.assertTrue(inspect.isasyncgen(async_gen_coro))
coro.close(); gen_coro.close(); # silence warnings
def test_isawaitable(self): def test_isawaitable(self):
def gen(): yield def gen(): yield

View File

@ -0,0 +1,3 @@
Make :func:`inspect.iscoroutinefunction`,
:func:`inspect.isgeneratorfunction` and :func:`inspect.isasyncgenfunction`
work with :func:`functools.partial`. Patch by Pablo Galindo.