https://github.com/python/cpython/commit/edb59d57188e5535729c3948d673d0de1b729c05
commit: edb59d57188e5535729c3948d673d0de1b729c05
branch: main
author: Martijn Pieters <[email protected]>
committer: encukou <[email protected]>
date: 2024-02-15T12:08:45+01:00
summary:

bpo-38364: unwrap partialmethods just like we unwrap partials (#16600)

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

Also: Rename _partialmethod to __partialmethod__.
Since we're checking this attribute on arbitrary function-like objects,
we should use the namespace reserved for core Python.

---------

Co-authored-by: Petr Viktorin <[email protected]>

files:
A Misc/NEWS.d/next/Library/2019-10-05-22-56-50.bpo-38364.sYTCWF.rst
M Doc/library/inspect.rst
M Lib/functools.py
M Lib/inspect.py
M Lib/test/test_inspect/test_inspect.py

diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index f8b3e39c4f54f0..8a74cadb98a0db 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -340,6 +340,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.13
+      Functions wrapped in :func:`functools.partialmethod` now return ``True``
+      if the wrapped function is a Python generator function.
 
 .. function:: isgenerator(object)
 
@@ -363,6 +366,10 @@ attributes (see :ref:`import-mod-attrs` for module 
attributes):
       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)
 
@@ -429,6 +436,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.13
+      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 55990e742bf23f..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):
@@ -424,6 +424,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 f0b72662a9a0b2..450093a8b4c1ee 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -383,8 +383,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)
@@ -2561,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 4611f62b293ff9..c5a6de5993fad4 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -206,12 +206,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(
@@ -220,6 +241,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():
@@ -285,6 +308,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(
@@ -297,6 +322,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(
@@ -311,6 +338,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
@@ -3389,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):
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``.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to