Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Oct 26, 2018
12 changes: 12 additions & 0 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ attributes:

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)

Expand All @@ -314,6 +318,10 @@ attributes:

.. 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)

Expand Down Expand Up @@ -355,6 +363,10 @@ attributes:

.. 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)

Expand Down
6 changes: 6 additions & 0 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,12 @@ def __get__(self, obj, cls):
def __isabstractmethod__(self):
return getattr(self.func, "__isabstractmethod__", False)

# Helper functions

def _unwrap_partial(func):
while isinstance(func, partial):
Copy link
Member

Choose a reason for hiding this comment

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

@pablogsal BTW this doesn't handle the case of partialmethod. Should we add that?

func = func.func
return func

################################################################################
### LRU Cache function decorator
Expand Down
21 changes: 12 additions & 9 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,30 +168,33 @@ def isfunction(object):
__kwdefaults__ dict of keyword only parameters with defaults"""
return isinstance(object, types.FunctionType)

def isgeneratorfunction(object):
def isgeneratorfunction(obj):
"""Return true if the object is a user-defined generator function.

Generator function objects provide the same attributes as functions.
See help(isfunction) for a list of attributes."""
return bool((isfunction(object) or ismethod(object)) and
object.__code__.co_flags & CO_GENERATOR)
obj = functools._unwrap_partial(obj)
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.

Coroutine functions are defined with "async def" syntax.
"""
return bool((isfunction(object) or ismethod(object)) and
object.__code__.co_flags & CO_COROUTINE)
obj = functools._unwrap_partial(obj)
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.

Asynchronous generator functions are defined with "async def"
syntax and have "yield" expressions in their body.
"""
return bool((isfunction(object) or ismethod(object)) and
object.__code__.co_flags & CO_ASYNC_GENERATOR)
obj = functools._unwrap_partial(obj)
return bool((isfunction(obj) or ismethod(obj)) and
obj.__code__.co_flags & CO_ASYNC_GENERATOR)

def isasyncgen(object):
"""Return true if the object is an asynchronous generator."""
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,8 +440,8 @@ async def func(x, y):

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

Expand Down
27 changes: 26 additions & 1 deletion Lib/test/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,26 +166,51 @@ def test_excluding_predicates(self):
self.assertFalse(inspect.ismemberdescriptor(datetime.timedelta.days))

def test_iscoroutine(self):
async_gen_coro = async_generator_function_example(1)
gen_coro = gen_coroutine_function_example(1)
coro = coroutine_function_example(1)

self.assertFalse(
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.assertTrue(
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.iscoroutinefunction(coroutine_function_example))
self.assertTrue(
inspect.iscoroutinefunction(
functools.partial(functools.partial(
coroutine_function_example))))
self.assertTrue(inspect.iscoroutine(coro))

self.assertFalse(
inspect.isgeneratorfunction(coroutine_function_example))
self.assertFalse(
inspect.isgeneratorfunction(
functools.partial(functools.partial(
coroutine_function_example))))
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 gen(): yield
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Make :func:`inspect.iscoroutinefunction`,
:func:`inspect.isgeneratorfunction` and :func:`inspect.isasyncgenfunction`
work with :func:`functools.partial`. Patch by Pablo Galindo.
Copy link
Member

Choose a reason for hiding this comment

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

Maybe also document the change with a ".. versionchanged:: 3.8" markup in https://docs.python.org/dev/library/inspect.html#inspect.isgeneratorfunction ?