From 9cb267acae2bc915659711e39c1cdf188448390b Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Sun, 6 Oct 2019 00:55:14 +0100 Subject: [PATCH 1/4] 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. --- Doc/library/inspect.rst | 10 ++++++++++ Lib/functools.py | 11 +++++++++++ Lib/inspect.py | 4 +++- Lib/test/test_inspect.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 58b84a35a890e3..39b1e3056b2920 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -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) @@ -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``. @@ -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) diff --git a/Lib/functools.py b/Lib/functools.py index 43ead512e1ea4e..c863fc78b0e2f7 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -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): + func = func._partialmethod + while isinstance(func, partialmethod): + func = getattr(func, 'func') + func = _unwrap_partial(func) + return func + ################################################################################ ### LRU Cache function decorator ################################################################################ diff --git a/Lib/inspect.py b/Lib/inspect.py index 3db7745e8a5eeb..536476e9d2905e 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -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`` set in its flags.""" + f = functools._unwrap_partialmethod(f) while ismethod(f): f = f.__func__ f = functools._unwrap_partial(f) diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 1b589c8df2a224..9589df4bf9ffbd 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -186,12 +186,33 @@ def test_iscoroutine(self): gen_coro = gen_coroutine_function_example(1) coro = coroutine_function_example(1) + class PMClass: + 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( @@ -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(): @@ -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( @@ -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( @@ -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 From 243cb00865e6e57be84c5a70003dd276439ffd6a Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Sun, 6 Oct 2019 00:01:44 +0100 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2019-10-05-22-56-50.bpo-38364.sYTCWF.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2019-10-05-22-56-50.bpo-38364.sYTCWF.rst diff --git a/Misc/NEWS.d/next/Library/2019-10-05-22-56-50.bpo-38364.sYTCWF.rst b/Misc/NEWS.d/next/Library/2019-10-05-22-56-50.bpo-38364.sYTCWF.rst new file mode 100644 index 00000000000000..87fb5ae8fd0eed --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-10-05-22-56-50.bpo-38364.sYTCWF.rst @@ -0,0 +1 @@ +The ``inspect`` functions ``isgeneratorfunction``, ``iscoroutinefunction``, ``isasyncgenfunction`` now support ``functools.partialmethod`` wrapped functions the same way they support ``functools.partial``. From d20e0696346da31ec802aa499e6dea71ca43613e Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 25 Jan 2024 14:52:58 +0100 Subject: [PATCH 3/4] Update versionchanged directives --- Doc/library/inspect.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 15c8dcae417645..8a74cadb98a0db 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -340,7 +340,7 @@ 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 + .. versionchanged:: 3.13 Functions wrapped in :func:`functools.partialmethod` now return ``True`` if the wrapped function is a Python generator function. @@ -362,14 +362,14 @@ 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``. + .. versionchanged:: 3.13 + Functions wrapped in :func:`functools.partialmethod` now return ``True`` + if the wrapped function is a :term:`coroutine function`. + .. function:: markcoroutinefunction(func) @@ -436,7 +436,7 @@ 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 + .. versionchanged:: 3.13 Functions wrapped in :func:`functools.partialmethod` now return ``True`` if the wrapped function is a :term:`coroutine function`. From fa36941d38b6d87ddb7715116f85a78254403028 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 25 Jan 2024 15:03:08 +0100 Subject: [PATCH 4/4] Rename _partialmethod to __partialmethod__ Since we'rs checking this attribute on arbitrary function-like objects, we should use the namespace reserved for core Python. --- Lib/functools.py | 6 +++--- Lib/inspect.py | 2 +- Lib/test/test_inspect/test_inspect.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index 2aa553d8193c47..ee4197b386178d 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -388,7 +388,7 @@ def _method(cls_or_self, /, *args, **keywords): keywords = {**self.keywords, **keywords} return self.func(cls_or_self, *self.args, *args, **keywords) _method.__isabstractmethod__ = self.__isabstractmethod__ - _method._partialmethod = self + _method.__partialmethod__ = self return _method def __get__(self, obj, cls=None): @@ -428,8 +428,8 @@ def _unwrap_partialmethod(func): prev = None while func is not prev: prev = func - while isinstance(getattr(func, "_partialmethod", None), partialmethod): - func = func._partialmethod + while isinstance(getattr(func, "__partialmethod__", None), partialmethod): + func = func.__partialmethod__ while isinstance(func, partialmethod): func = getattr(func, 'func') func = _unwrap_partial(func) diff --git a/Lib/inspect.py b/Lib/inspect.py index 5e57e8809fa010..450093a8b4c1ee 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -2563,7 +2563,7 @@ def _signature_from_callable(obj, *, return sig try: - partialmethod = obj._partialmethod + partialmethod = obj.__partialmethod__ except AttributeError: pass else: diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 58316bb6f49a10..c5a6de5993fad4 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3418,7 +3418,7 @@ def test(self: 'anno', x): def test_signature_on_fake_partialmethod(self): def foo(a): pass - foo._partialmethod = 'spam' + foo.__partialmethod__ = 'spam' self.assertEqual(str(inspect.signature(foo)), '(a)') def test_signature_on_decorated(self):