Skip to content

asyncio.run unnecessarily calls the repr of the task result twice since Python 3.11 #112559

Closed
@yilei

Description

@yilei

Bug report

Bug description:

Given the following code:

import asyncio
import time

class Foo:
    def __repr__(self):
        time.sleep(1)
        print('i am a repr, i should not be called. ')
        return '<Foo>'


async def get_foo():
    return Foo()

asyncio.run(get_foo())
print('Done')

Output:

$ python t.py
i am a repr, i should not be called.
i am a repr, i should not be called.
Done

This was caused by the new SIGINT handler installed by asyncio.run here: f08a191

Upon investigation, changing the code with:

import asyncio
import time

class Foo:
    def __repr__(self):
        time.sleep(1)
        print('i am a repr, i should not be called. ')
        raise BaseException('where is this called???????')
        return '<Foo>'


async def get_foo():
    return Foo()

asyncio.run(get_foo())
print('Done')

It shows:

i am a repr, i should not be called.
Traceback (most recent call last):
  File "t.py", line 15, in <module>
    asyncio.run(get_foo())
  File "lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "lib/python3.12/asyncio/runners.py", line 127, in run
    and signal.getsignal(signal.SIGINT) is sigint_handler
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/signal.py", line 63, in getsignal
    return _int_to_enum(handler, Handlers)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/signal.py", line 29, in _int_to_enum
    return enum_klass(value)
           ^^^^^^^^^^^^^^^^^
  File "lib/python3.12/enum.py", line 740, in __call__
    return cls.__new__(cls, value)
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/enum.py", line 1152, in __new__
    ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
                                                  ^^^^^
  File "lib/python3.12/reprlib.py", line 21, in wrapper
    result = user_function(self)
             ^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/asyncio/base_tasks.py", line 30, in _task_repr
    info = ' '.join(_task_repr_info(task))
                    ^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/asyncio/base_tasks.py", line 10, in _task_repr_info
    info = base_futures._future_repr_info(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/asyncio/base_futures.py", line 54, in _future_repr_info
    result = reprlib.repr(future._result)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/reprlib.py", line 58, in repr
    return self.repr1(x, self.maxlevel)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/reprlib.py", line 68, in repr1
    return self.repr_instance(x, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "lib/python3.12/reprlib.py", line 170, in repr_instance
    s = builtins.repr(x)
        ^^^^^^^^^^^^^^^^
  File "t.py", line 8, in __repr__
    raise BaseException('where is this called???????')
BaseException: where is this called???????

This looks like a series unfortunate events, running on 3.12.0:

  1. signal.getsignal tries to convert the handler function to enum in case this is part of Handlers:
    return enum_klass(value)
  2. enum raises a ValueError with the repr of the handler function:
    ve_exc = ValueError("%r is not a valid %s" % (value, cls.__qualname__))
  3. the handler asyncio.run installs uses a functools.partial, it's repr will include the repr of the task:
    sigint_handler = functools.partial(self._on_sigint, main_task=task)
  4. when the repr is actually called at the end (one in signal.getsignal, the other in signal.signal), the repr of the asyncio task will include the repr of the result:
    and signal.getsignal(signal.SIGINT) is sigint_handler
    ):
    signal.signal(signal.SIGINT, signal.default_int_handler)

While one can argument calling __repr__ shouldn't cause issues, but I think we could avoid them in the signal._int_to_enum function completely, by only trying to convert to enum when it's an integer:

def _int_to_enum(value, enum_klass):
    """Convert a numeric value to an IntEnum member.
    If it's not a known member, return the numeric value itself.
    """
    if not isinstance(value, int):
        return value
    try:
        return enum_klass(value)
    except ValueError:
        return value

This should be more efficient on its own anyway.

This function's doc is also inaccurate, since it also accepts non integers (usually a callable).

Am I missing something?

CPython versions tested on:

3.11, 3.12

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.11only security fixes3.12only security fixes3.13bugs and security fixesstdlibPython modules in the Lib dirtopic-asynciotype-bugAn unexpected behavior, bug, or error

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions