Skip to content

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

@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

      pFad - Phonifier reborn

      Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

      Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


      Alternative Proxies:

      Alternative Proxy

      pFad Proxy

      pFad v3 Proxy

      pFad v4 Proxy