diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index aff165191b76e8..b1112a9a48c89e 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -302,7 +302,8 @@ Miscellaneous options .. option:: -i - Enter interactive mode after execution. + Enter interactive mode after execution, or force interactive mode even when + :data:`sys.stdin` does not appear to be a terminal. Using the :option:`-i` option will enter interactive mode in any of the following circumstances\: @@ -310,8 +311,13 @@ Miscellaneous options * When the :option:`-c` option is used * When the :option:`-m` option is used - Interactive mode will start even when :data:`sys.stdin` does not appear to be a terminal. The - :envvar:`PYTHONSTARTUP` file is not read. + In these "execute then interact" cases, Python runs the script or command + first and does not read the :envvar:`PYTHONSTARTUP` file before entering + interactive mode. + + When :option:`-i` is used only to force interactive mode despite redirected + standard input (for example, ``python -i < /dev/null``), the interpreter + enters interactive mode directly and reads :envvar:`PYTHONSTARTUP` as usual. This can be useful to inspect global variables or a stack trace when a script raises an exception. See also :envvar:`PYTHONINSPECT`. diff --git a/Lib/inspect.py b/Lib/inspect.py index ff462750888c88..4b24417925c273 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -306,10 +306,28 @@ def isgeneratorfunction(obj): _is_coroutine_mark = object() def _has_coroutine_mark(f): - while ismethod(f): - f = f.__func__ - f = functools._unwrap_partial(f) - return getattr(f, "_is_coroutine_marker", None) is _is_coroutine_mark + while True: + # Methods: unwrap first (methods cannot be coroutine-marked) + if ismethod(f): + f = f.__func__ + continue + + # Direct marker check + if getattr(f, "_is_coroutine_marker", None) is _is_coroutine_mark: + return True + + # Functions created by partialmethod descriptors keep a __partialmethod__ reference + pm = getattr(f, "__partialmethod__", None) + if isinstance(pm, functools.partialmethod): + f = pm + continue + + # partial and partialmethod share .func + if isinstance(f, (functools.partial, functools.partialmethod)): + f = f.func + continue + + return False def markcoroutinefunction(func): """ diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index dd3b7d9c5b4b5b..8120ceb9397b0e 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -380,6 +380,27 @@ def do_something_static(): coro.close(); gen_coro.close(); # silence warnings + def test_marked_partials_are_coroutinefunctions(self): + def regular_function(): + pass + + marked_partial = inspect.markcoroutinefunction( + functools.partial(regular_function)) + self.assertTrue(inspect.iscoroutinefunction(marked_partial)) + self.assertFalse( + inspect.iscoroutinefunction(functools.partial(regular_function))) + + class PMClass: + def method(self, /): + pass + + marked = inspect.markcoroutinefunction( + functools.partialmethod(method)) + unmarked = functools.partialmethod(method) + + self.assertTrue(inspect.iscoroutinefunction(PMClass.marked)) + self.assertFalse(inspect.iscoroutinefunction(PMClass.unmarked)) + def test_isawaitable(self): def gen(): yield self.assertFalse(inspect.isawaitable(gen())) diff --git a/Misc/NEWS.d/next/Library/2025-12-10-02-30-00.gh-issue-142418.j9Zka1.rst b/Misc/NEWS.d/next/Library/2025-12-10-02-30-00.gh-issue-142418.j9Zka1.rst new file mode 100644 index 00000000000000..fe4596a362eeda --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-10-02-30-00.gh-issue-142418.j9Zka1.rst @@ -0,0 +1,4 @@ +Fix ``inspect.iscoroutinefunction()`` incorrectly returning ``False`` for +callables wrapped in ``functools.partial`` or ``functools.partialmethod`` when +explicitly marked with ``inspect.markcoroutinefunction()``. The function now +detects coroutine markers on wrappers at each unwrap stage.