python / cpython

The Python programming language
https://www.python.org
Other
63.36k stars 30.34k forks source link

Exception copy error #87626

Open 321b4b3e-75d5-44c9-b8ef-7555e8fd41ba opened 3 years ago

321b4b3e-75d5-44c9-b8ef-7555e8fd41ba commented 3 years ago
BPO 43460
Nosy @iritkatriel, @douglas-raillard-arm

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields: ```python assignee = None closed_at = None created_at = labels = ['type-bug', 'library', '3.11'] title = 'Exception copy error' updated_at = user = 'https://github.com/douglas-raillard-arm' ``` bugs.python.org fields: ```python activity = actor = 'iritkatriel' assignee = 'none' closed = False closed_date = None closer = None components = ['Library (Lib)'] creation = creator = 'douglas-raillard-arm' dependencies = [] files = [] hgrepos = [] issue_num = 43460 keywords = [] message_count = 3.0 messages = ['388427', '388428', '396600'] nosy_count = 2.0 nosy_names = ['iritkatriel', 'douglas-raillard-arm'] pr_nums = [] priority = 'normal' resolution = None stage = None status = 'open' superseder = None type = 'behavior' url = 'https://bugs.python.org/issue43460' versions = ['Python 3.11'] ```

321b4b3e-75d5-44c9-b8ef-7555e8fd41ba commented 3 years ago

Instances of subclasses of BaseException created with keyword argument fail to copy properly as demonstrated by:

    import copy

    class E(BaseException):
        def __init__(self, x):
            self.x=x

    # works fine
    e = E(None)
    copy.copy(e)

    # raises
    e = E(x=None)
    copy.copy(e)

This seems to affect all Python versions I've tested (3.6 \<= Python \<= 3.9).

I've currently partially worked around the issue with a custom pickler that just restores __dict__, but:

One the root of the issue:

It seems that the current behavior is a consequence of the __dict__ being created lazily, I assume for speed and memory efficiency

There seems to be a few approaches that would solve the issue:

[1] https://github.com/python/cpython/blob/master/Objects/exceptions.c#L134

321b4b3e-75d5-44c9-b8ef-7555e8fd41ba commented 3 years ago

The solution based on the signature is something along those lines:

    class E(BaseException):
        def __new__(cls, *args, **kwargs):
            """
            Fix exception copying.
        Turn all the keyword arguments into positional arguments, so that the
        :exc:`BaseException` machinery has all the parameters for a valid call
        to ``__new__``, instead of missing all the keyword arguments.
        """
        sig = inspect.signature(cls.__init__)
        bound_args = sig.bind_partial(*args, **kwargs)
        bound_args.apply_defaults()
        args = tuple(bound_args.arguments.values())
        return super().__new__(cls, *args)
        def __init__(self, x):
            self.x=x

But there are a many shortcomings to that approach:

iritkatriel commented 3 years ago

See bpo-32696, bpo-30005, bpo-29466