New submission from Douglas Raillard <douglas.raill...@arm.com>:

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:

 * "args" is not part of __dict__, and setting "args" key in __dict__ does not 
create a "working object" (i.e. the key is set, but is ignored for all intents 
and purposes except direct lookup in __dict__)

 * pickle is friendly: you can provide a custom pickler that chooses the reduce 
function for each single class.
   copy module is much less friendly: copyreg.pickle() only allow registering 
custom functions for specific classes. That means there is no way (that I know) 
to make copy.copy() select a custom reduce for a whole subclass tree.


One the root of the issue:

 * exception from the standard library prevent keyword arguments (maybe because 
of that issue ?), but there is no such restriction on user-defined classes.
 * the culprit is BaseException_reduce() (in Objects/exceptions.c) [1]

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:

 * keyword arguments passed to the constructor could be fused with the 
positional arguments in BaseException_new (using the signature, but signature 
might be not be available for extension types I suppose)

 * keyword arguments could just be stored like "args" in a "kwargs" attribute 
in PyException_HEAD, so they are preserved and passed again to __new__ when the 
instance is restored upon copying/pickling.

 * the fact that keyword arguments were used could be saved as a bool in 
PyException_HEAD. When set, this flag would make BaseException_reduce() only 
use __dict__ and not "args". This would technically probably be a breaking 
change, but the only cases I can think of where this would be observable are a 
bit far fetched (if __new__ or __init__ have side effects beyond storing 
attributes in __dict__).

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

----------
messages: 388427
nosy: douglas-raillard-arm
priority: normal
severity: normal
status: open
title: Exception copy  error
type: behavior

_______________________________________
Python tracker <rep...@bugs.python.org>
<https://bugs.python.org/issue43460>
_______________________________________
_______________________________________________
Python-bugs-list mailing list
Unsubscribe: 
https://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com

Reply via email to