From 633c49796e6f382ad031628f59f4e98f7bf54d7b Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Mon, 15 Oct 2018 23:11:11 +0100 Subject: [PATCH 1/8] Make iscoroutinefunction, isgeneratorfunction and isasyncgenfunction work with partial --- Lib/inspect.py | 18 +++++++++++---- Lib/test/test_inspect.py | 22 ++++++++++++++++++- .../2018-10-15-23-10-41.bpo-34890.77E770.rst | 3 +++ 3 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2018-10-15-23-10-41.bpo-34890.77E770.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 857892bc8144c5..5f2c0aa0048c01 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -173,16 +173,22 @@ def isgeneratorfunction(object): 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 + is_generator_function = bool((isfunction(object) or ismethod(object)) and object.__code__.co_flags & CO_GENERATOR) + is_partial_generator = bool((isinstance(object, functools.partial) and + object.func.__code__.co_flags & CO_GENERATOR)) + return is_generator_function or is_partial_generator def iscoroutinefunction(object): """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) + is_coroutine_function = bool(((isfunction(object) or ismethod(object)) and + object.__code__.co_flags & CO_COROUTINE)) + is_partial_coroutine = bool((isinstance(object, functools.partial) and + object.func.__code__.co_flags & CO_COROUTINE)) + return is_coroutine_function or is_partial_coroutine def isasyncgenfunction(object): """Return true if the object is an asynchronous generator function. @@ -190,8 +196,12 @@ def isasyncgenfunction(object): Asynchronous generator functions are defined with "async def" syntax and have "yield" expressions in their body. """ - return bool((isfunction(object) or ismethod(object)) and + is_async_gen_function = bool((isfunction(object) or ismethod(object)) and object.__code__.co_flags & CO_ASYNC_GENERATOR) + is_partial_async_gen = bool((isinstance(object, functools.partial) and + object.func.__code__.co_flags & CO_ASYNC_GENERATOR)) + return is_async_gen_function or is_partial_async_gen + def isasyncgen(object): """Return true if the object is an asynchronous generator.""" diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 134b0cd0b735c4..821173f2dce4a8 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -166,26 +166,46 @@ 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(gen_coroutine_function_example))) self.assertFalse(inspect.iscoroutine(gen_coro)) self.assertTrue( inspect.isgeneratorfunction(gen_coroutine_function_example)) + self.assertTrue( + inspect.isgeneratorfunction( + 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(coroutine_function_example))) self.assertTrue(inspect.iscoroutine(coro)) self.assertFalse( inspect.isgeneratorfunction(coroutine_function_example)) + self.assertFalse( + inspect.isgeneratorfunction( + 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(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 diff --git a/Misc/NEWS.d/next/Library/2018-10-15-23-10-41.bpo-34890.77E770.rst b/Misc/NEWS.d/next/Library/2018-10-15-23-10-41.bpo-34890.77E770.rst new file mode 100644 index 00000000000000..58745b2895918d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-10-15-23-10-41.bpo-34890.77E770.rst @@ -0,0 +1,3 @@ +Make :func:`inspect.iscoroutinefunction`, +:func:`inspect.isgeneratorfunction` and :func:`inspect.isasyncgenfunction` +work with :func:`functools.partial`. Patch by Pablo Galindo. From 41f7f600dfa5a5785f329acd2197ce33f5189553 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Mon, 15 Oct 2018 23:24:33 +0100 Subject: [PATCH 2/8] Simplify code, rename variables and add test for nested partials --- Lib/inspect.py | 34 +++++++++++++++------------------- Lib/test/test_inspect.py | 11 ++++++----- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 5f2c0aa0048c01..009e9d90311b9e 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -168,40 +168,36 @@ 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.""" - is_generator_function = bool((isfunction(object) or ismethod(object)) and - object.__code__.co_flags & CO_GENERATOR) - is_partial_generator = bool((isinstance(object, functools.partial) and - object.func.__code__.co_flags & CO_GENERATOR)) - return is_generator_function or is_partial_generator + while isinstance(obj, functools.partial): + obj = obj.func + 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. """ - is_coroutine_function = bool(((isfunction(object) or ismethod(object)) and - object.__code__.co_flags & CO_COROUTINE)) - is_partial_coroutine = bool((isinstance(object, functools.partial) and - object.func.__code__.co_flags & CO_COROUTINE)) - return is_coroutine_function or is_partial_coroutine + while isinstance(obj, functools.partial): + obj = obj.func + 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. """ - is_async_gen_function = bool((isfunction(object) or ismethod(object)) and - object.__code__.co_flags & CO_ASYNC_GENERATOR) - is_partial_async_gen = bool((isinstance(object, functools.partial) and - object.func.__code__.co_flags & CO_ASYNC_GENERATOR)) - return is_async_gen_function or is_partial_async_gen - + while isinstance(obj, functools.partial): + obj = obj.func + 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.""" diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 821173f2dce4a8..f68089be823cd7 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -2,6 +2,7 @@ import collections import datetime import functools +from functools import partial import importlib import inspect import io @@ -174,35 +175,35 @@ def test_iscoroutine(self): inspect.iscoroutinefunction(gen_coroutine_function_example)) self.assertFalse( inspect.iscoroutinefunction( - functools.partial(gen_coroutine_function_example))) + partial(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(gen_coroutine_function_example))) + partial(partial(gen_coroutine_function_example)))) self.assertTrue(inspect.isgenerator(gen_coro)) self.assertTrue( inspect.iscoroutinefunction(coroutine_function_example)) self.assertTrue( inspect.iscoroutinefunction( - functools.partial(coroutine_function_example))) + partial(partial(coroutine_function_example)))) self.assertTrue(inspect.iscoroutine(coro)) self.assertFalse( inspect.isgeneratorfunction(coroutine_function_example)) self.assertFalse( inspect.isgeneratorfunction( - functools.partial(coroutine_function_example))) + partial(partial(coroutine_function_example)))) self.assertFalse(inspect.isgenerator(coro)) self.assertTrue( inspect.isasyncgenfunction(async_generator_function_example)) self.assertTrue( inspect.isasyncgenfunction( - functools.partial(async_generator_function_example))) + partial(partial(async_generator_function_example)))) self.assertTrue(inspect.isasyncgen(async_gen_coro)) coro.close(); gen_coro.close(); # silence warnings From b65500e7d714b3dd8b9177d1454d3c7446fd7940 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Mon, 15 Oct 2018 23:31:49 +0100 Subject: [PATCH 3/8] fixup! Simplify code, rename variables and add test for nested partials --- Lib/inspect.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 009e9d90311b9e..9a36eacf6cede2 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -174,7 +174,7 @@ def isgeneratorfunction(obj): Generator function objects provide the same attributes as functions. See help(isfunction) for a list of attributes.""" while isinstance(obj, functools.partial): - obj = obj.func + obj = obj.func return bool((isfunction(obj) or ismethod(obj)) and obj.__code__.co_flags & CO_GENERATOR) @@ -195,7 +195,7 @@ def isasyncgenfunction(obj): syntax and have "yield" expressions in their body. """ while isinstance(obj, functools.partial): - obj = obj.func + obj = obj.func return bool((isfunction(obj) or ismethod(obj)) and obj.__code__.co_flags & CO_ASYNC_GENERATOR) From 3b841aceff5e48b5c6f4d8cf95c8accd6143cb44 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Mon, 15 Oct 2018 23:41:07 +0100 Subject: [PATCH 4/8] Add _unwrap_partial helper to reduce repetition --- Lib/inspect.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 9a36eacf6cede2..7508ec3ccb5801 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -59,6 +59,11 @@ # See Include/object.h TPFLAGS_IS_ABSTRACT = 1 << 20 +def _unwrap_partial(func): + while isinstance(func, functools.partial): + func = func.func + return func + # ----------------------------------------------------------- type-checking def ismodule(object): """Return true if the object is a module. @@ -173,8 +178,7 @@ def isgeneratorfunction(obj): Generator function objects provide the same attributes as functions. See help(isfunction) for a list of attributes.""" - while isinstance(obj, functools.partial): - obj = obj.func + obj = _unwrap_partial(obj) return bool((isfunction(obj) or ismethod(obj)) and obj.__code__.co_flags & CO_GENERATOR) @@ -183,8 +187,7 @@ def iscoroutinefunction(obj): Coroutine functions are defined with "async def" syntax. """ - while isinstance(obj, functools.partial): - obj = obj.func + obj = _unwrap_partial(obj) return bool(((isfunction(obj) or ismethod(obj)) and obj.__code__.co_flags & CO_COROUTINE)) @@ -194,8 +197,7 @@ def isasyncgenfunction(obj): Asynchronous generator functions are defined with "async def" syntax and have "yield" expressions in their body. """ - while isinstance(obj, functools.partial): - obj = obj.func + obj = _unwrap_partial(obj) return bool((isfunction(obj) or ismethod(obj)) and obj.__code__.co_flags & CO_ASYNC_GENERATOR) From adb5db66a9519281510d9e1e3d54bb7708476099 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Mon, 15 Oct 2018 23:52:00 +0100 Subject: [PATCH 5/8] Use module import in tests --- Lib/test/test_inspect.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index f68089be823cd7..b9072e0137eb08 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -2,7 +2,6 @@ import collections import datetime import functools -from functools import partial import importlib import inspect import io @@ -175,35 +174,40 @@ def test_iscoroutine(self): inspect.iscoroutinefunction(gen_coroutine_function_example)) self.assertFalse( inspect.iscoroutinefunction( - partial(partial(gen_coroutine_function_example)))) + 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( - partial(partial(gen_coroutine_function_example)))) + 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( - partial(partial(coroutine_function_example)))) + functools.partial(functools.partial( + coroutine_function_example)))) self.assertTrue(inspect.iscoroutine(coro)) self.assertFalse( inspect.isgeneratorfunction(coroutine_function_example)) self.assertFalse( inspect.isgeneratorfunction( - partial(partial(coroutine_function_example)))) + functools.partial(functools.partial( + coroutine_function_example)))) self.assertFalse(inspect.isgenerator(coro)) self.assertTrue( inspect.isasyncgenfunction(async_generator_function_example)) self.assertTrue( inspect.isasyncgenfunction( - partial(partial(async_generator_function_example)))) + functools.partial(functools.partial( + async_generator_function_example)))) self.assertTrue(inspect.isasyncgen(async_gen_coro)) coro.close(); gen_coro.close(); # silence warnings From 617688e1cb9a20edb2be11c56ea224c0d3789657 Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Tue, 16 Oct 2018 00:19:08 +0100 Subject: [PATCH 6/8] Add extra check to Lib/asyncio/coroutines:coroutine for partial to preserve behaviour --- Lib/asyncio/coroutines.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/coroutines.py b/Lib/asyncio/coroutines.py index c665ebe33ee196..04c788ac18f47c 100644 --- a/Lib/asyncio/coroutines.py +++ b/Lib/asyncio/coroutines.py @@ -107,12 +107,14 @@ def coroutine(func): If the coroutine is not yielded from before it is destroyed, an error message is logged. """ - if inspect.iscoroutinefunction(func): + if (inspect.iscoroutinefunction(func) + and not isinstance(func, functools.partial)): # In Python 3.5 that's all we need to do for coroutines # defined with "async def". return func - if inspect.isgeneratorfunction(func): + if (inspect.isgeneratorfunction(func) + and not isinstance (func, functools.partial)): coro = func else: @functools.wraps(func) From caddd218ab8277c5e32e0bd8b586ce83662180ca Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Tue, 16 Oct 2018 09:22:31 +0100 Subject: [PATCH 7/8] Change test that checks __reprl__ of partial --- Lib/asyncio/coroutines.py | 6 ++---- Lib/test/test_asyncio/test_tasks.py | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/asyncio/coroutines.py b/Lib/asyncio/coroutines.py index 04c788ac18f47c..c665ebe33ee196 100644 --- a/Lib/asyncio/coroutines.py +++ b/Lib/asyncio/coroutines.py @@ -107,14 +107,12 @@ def coroutine(func): If the coroutine is not yielded from before it is destroyed, an error message is logged. """ - if (inspect.iscoroutinefunction(func) - and not isinstance(func, functools.partial)): + if inspect.iscoroutinefunction(func): # In Python 3.5 that's all we need to do for coroutines # defined with "async def". return func - if (inspect.isgeneratorfunction(func) - and not isinstance (func, functools.partial)): + if inspect.isgeneratorfunction(func): coro = func else: @functools.wraps(func) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 0fe767630f1a99..c65d1f2440d2e6 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -440,8 +440,8 @@ async def func(x, y): coro_repr = repr(task._coro) expected = ( - r'\.func\(1\)\(\) running, ' + r'\.func at' ) self.assertRegex(coro_repr, expected) From 0585757eb495fce622c975819534f487b4bdac6f Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Thu, 25 Oct 2018 23:25:02 +0100 Subject: [PATCH 8/8] Refactor helper function and document changes --- Doc/library/inspect.rst | 12 ++++++++++++ Lib/functools.py | 6 ++++++ Lib/inspect.py | 11 +++-------- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 523a5f34179b81..79efe482a29723 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -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) @@ -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) @@ -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) diff --git a/Lib/functools.py b/Lib/functools.py index 51048f5946c346..0ea9d9eb52ba62 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -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): + func = func.func + return func ################################################################################ ### LRU Cache function decorator diff --git a/Lib/inspect.py b/Lib/inspect.py index 7508ec3ccb5801..09652963dcf0f6 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -59,11 +59,6 @@ # See Include/object.h TPFLAGS_IS_ABSTRACT = 1 << 20 -def _unwrap_partial(func): - while isinstance(func, functools.partial): - func = func.func - return func - # ----------------------------------------------------------- type-checking def ismodule(object): """Return true if the object is a module. @@ -178,7 +173,7 @@ def isgeneratorfunction(obj): Generator function objects provide the same attributes as functions. See help(isfunction) for a list of attributes.""" - obj = _unwrap_partial(obj) + obj = functools._unwrap_partial(obj) return bool((isfunction(obj) or ismethod(obj)) and obj.__code__.co_flags & CO_GENERATOR) @@ -187,7 +182,7 @@ def iscoroutinefunction(obj): Coroutine functions are defined with "async def" syntax. """ - obj = _unwrap_partial(obj) + obj = functools._unwrap_partial(obj) return bool(((isfunction(obj) or ismethod(obj)) and obj.__code__.co_flags & CO_COROUTINE)) @@ -197,7 +192,7 @@ def isasyncgenfunction(obj): Asynchronous generator functions are defined with "async def" syntax and have "yield" expressions in their body. """ - obj = _unwrap_partial(obj) + obj = functools._unwrap_partial(obj) return bool((isfunction(obj) or ismethod(obj)) and obj.__code__.co_flags & CO_ASYNC_GENERATOR)