Skip to content
Open
12 changes: 9 additions & 3 deletions Doc/using/cmdline.rst
Original file line number Diff line number Diff line change
Expand Up @@ -302,16 +302,22 @@ 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\:

* When a script is passed as first argument
* 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
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems unrelated changes

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`.
Expand Down
26 changes: 22 additions & 4 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Member

@picnixz picnixz Dec 14, 2025

Choose a reason for hiding this comment

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

Please:

  • Wrap all lines under 80 characters.
  • Remove "obvious" comments. "Methods: unwrap first" is clear from the way you're doing it.
  • Avoid blank lines. The standard library usually tries to avoid expanding the code vertically.

# Methods: unwrap first (methods cannot be coroutine-marked)
if ismethod(f):
Copy link
Member

Choose a reason for hiding this comment

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

Why not doing while ismethod(f): f = f.__func__ here?

Choose a reason for hiding this comment

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

>>> from functools import partialmethod
>>> class MyClass:
...     def a(self): ...
...     b = partialmethod(a)
>>> obj = MyClass()
>>> obj.b
functools.partial(<bound method MyClass.a of <__main__.MyClass object at 0x7fd343f4cc20>>)
>>> obj.b.func.__func__
<function MyClass.a at 0x7fd343f42cf0>

When unwrapping the reference to obj.b, the functools.partial object comes before the method object, so the assumption that method objects always precede other objects is incorrect.

>>> from functools import partialmethod
>>> class MyFirstClass:
...     def f(self, other): ...
>>> first = MyFirstClass()
>>> class MySecondClass:
...     g = partialmethod(first.f)
>>> second = MySecondClass()
>>> second.g
<bound method partialmethod._make_unbound_method.<locals>._method of <__main__.MySecondClass object at 0x7fd343f4d400>>
>>> second.g.__func__.__partialmethod__.func
<bound method MyFirstClass.f of <__main__.MyFirstClass object at 0x7fd343f4d2b0>>

Well, it seems to me that the current implementation (in the main branch) is not very well thought out. Personally, I would not rely on it.

Copy link

@x42005e1f x42005e1f Dec 14, 2025

Choose a reason for hiding this comment

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

If your comment is only about why there is if instead of while... why do we need a nested loop when we already have an outer one? Moreover, how often are methods that refer to other methods used? Does it even make sense to try to optimize this case? I think it is sufficient and simpler to rely on the outer loop.

As you can easily see, the code in this PR is very similar to the one I attached to the original issue, except for the order of the blocks and some points borrowed from the original code (AI-generated or just copy-paste?). I use something similar in the implementation of similar functions in my library, although they are more general in nature.

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
Comment on lines +319 to +323

Choose a reason for hiding this comment

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

Well, this can also be moved forward by one block to avoid the time spent on obtaining the attribute when it is not necessary. I hope I have not bored you with these micro-optimizations.

Comment on lines +319 to +323
Copy link
Member

Choose a reason for hiding this comment

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

I think you should use functools._unwrap_partialmethod which handles both partial methods and partial functions (first it unwraps partial methods then unwraps partial functions)

Choose a reason for hiding this comment

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

Please read the discussion at #142505.


# partial and partialmethod share .func
if isinstance(f, (functools.partial, functools.partialmethod)):
Copy link

@x42005e1f x42005e1f Dec 10, 2025

Choose a reason for hiding this comment

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

I will also add, "for the record", why I do not handle partialmethod objects in this way in the code attached to the original issue. They are not callable, and therefore applying markcoroutinefunction() to them is incorrect, which means they should not be checked. Being defined as a class member, accessing the corresponding attribute will return a regular function object created by partialmethod (or a method object for such a function, if via an instance). Therefore, there is no point in unnecessary iteration, and you can go straight to pm.func (see the block above).

f = f.func
continue

return False

def markcoroutinefunction(func):
"""
Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_inspect/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading