Skip to content

gh-93627: Align copy module behaviour with pickle module #109498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 18 additions & 16 deletions Lib/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class Error(Exception):
pass
error = Error # backward compatibility

_NoValue = object()

__all__ = ["Error", "copy", "deepcopy", "replace"]

def copy(x):
Expand All @@ -75,20 +77,20 @@ def copy(x):
# treat it as a regular class:
return _copy_immutable(x)

copier = getattr(cls, "__copy__", None)
if copier is not None:
copier = getattr(cls, "__copy__", _NoValue)
if copier is not _NoValue:
return copier(x)

reductor = dispatch_table.get(cls)
if reductor is not None:
reductor = dispatch_table.get(cls, _NoValue)
if reductor is not _NoValue:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor is not None:
reductor = getattr(x, "__reduce_ex__", _NoValue)
if reductor is not _NoValue:
rv = reductor(4)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
reductor = getattr(x, "__reduce__", _NoValue)
if reductor is not _NoValue:
rv = reductor()
else:
raise Error("un(shallow)copyable object of type %s" % cls)
Expand Down Expand Up @@ -142,20 +144,20 @@ def deepcopy(x, memo=None, _nil=[]):
if issubclass(cls, type):
y = x # atomic copy
else:
copier = getattr(x, "__deepcopy__", None)
if copier is not None:
copier = getattr(x, "__deepcopy__", _NoValue)
if copier is not _NoValue:
y = copier(memo)
else:
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor is not None:
reductor = getattr(x, "__reduce_ex__", _NoValue)
if reductor is not _NoValue:
rv = reductor(4)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
reductor = getattr(x, "__reduce__", _NoValue)
if reductor is not _NoValue:
rv = reductor()
else:
raise Error(
Expand Down Expand Up @@ -289,7 +291,7 @@ def replace(obj, /, **changes):
frozen dataclasses.
"""
cls = obj.__class__
func = getattr(cls, '__replace__', None)
if func is None:
func = getattr(cls, '__replace__', _NoValue)
if func is _NoValue:
raise TypeError(f"replace() does not support {cls.__name__} objects")
return func(obj, **changes)
27 changes: 27 additions & 0 deletions Lib/test/test_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ def __reduce__(self):
self.assertIs(y, x)
self.assertEqual(c, [1])

def test_copy_invalid_reduction_methods(self):
class C(object):
__copy__ = None
x = C()
with self.assertRaises(TypeError):
copy.copy(x)

class C(object):
__reduce_ex__ = None
x = C()
with self.assertRaises(TypeError):
copy.copy(x)

class C(object):
__reduce_ex__ = copy._NoValue
__reduce__ = None
x = C()
with self.assertRaises(TypeError):
copy.copy(x)

def test_copy_reduce(self):
class C(object):
def __reduce__(self):
Expand Down Expand Up @@ -974,6 +994,13 @@ class C:
with self.assertRaisesRegex(TypeError, 'unexpected keyword argument'):
copy.replace(c, x=1, error=2)

def test_invalid_replace_method(self):
class A:
__replace__ = None
a = A()
with self.assertRaises(TypeError):
copy.replace(a)


class MiscTestCase(unittest.TestCase):
def test__all__(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Update the Python copy module implementation to match the implementation of the pickle module. For objects setting reduction methods like :meth:`~object.__copy__` , :meth:`~object.__reduce_ex__` or :meth:`~object.__reduce__` to ``None``, a call to :meth:`copy.copy` or :meth:`copy.deepcopy` will result in a :exc:`TypeError`.
Loading