Skip to content

gh-108751: Add copy.replace() function #108752

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

Merged
merged 2 commits into from
Sep 6, 2023
Merged
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
2 changes: 2 additions & 0 deletions Doc/library/collections.rst
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,8 @@ field names, the method and attribute names start with an underscore.
>>> for partnum, record in inventory.items():
... inventory[partnum] = record._replace(price=newprices[partnum], timestamp=time.now())

Named tuples are also supported by generic function :func:`copy.replace`.

.. attribute:: somenamedtuple._fields

Tuple of strings listing the field names. Useful for introspection
Expand Down
30 changes: 26 additions & 4 deletions Doc/library/copy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,22 @@ operations (explained below).

Interface summary:

.. function:: copy(x)
.. function:: copy(obj)

Return a shallow copy of *x*.
Return a shallow copy of *obj*.


.. function:: deepcopy(x[, memo])
.. function:: deepcopy(obj[, memo])

Return a deep copy of *x*.
Return a deep copy of *obj*.


.. function:: replace(obj, /, **changes)

Creates a new object of the same type as *obj*, replacing fields with values
from *changes*.

.. versionadded:: 3.13


.. exception:: Error
Expand Down Expand Up @@ -89,6 +97,20 @@ with the component as first argument and the memo dictionary as second argument.
The memo dictionary should be treated as an opaque object.


.. index::
single: __replace__() (replace protocol)

Function :func:`replace` is more limited than :func:`copy` and :func:`deepcopy`,
and only supports named tuples created by :func:`~collections.namedtuple`,
:mod:`dataclasses`, and other classes which define method :meth:`!__replace__`.

.. method:: __replace__(self, /, **changes)
:noindex:

:meth:`!__replace__` should create a new object of the same type,
replacing fields with values from *changes*.


.. seealso::

Module :mod:`pickle`
Expand Down
2 changes: 2 additions & 0 deletions Doc/library/dataclasses.rst
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ Module contents
``replace()`` (or similarly named) method which handles instance
copying.

Dataclass instances are also supported by generic function :func:`copy.replace`.

.. function:: is_dataclass(obj)

Return ``True`` if its parameter is a dataclass or an instance of one,
Expand Down
9 changes: 9 additions & 0 deletions Doc/library/datetime.rst
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,9 @@ Instance methods:
>>> d.replace(day=26)
datetime.date(2002, 12, 26)

:class:`date` objects are also supported by generic function
:func:`copy.replace`.


.. method:: date.timetuple()

Expand Down Expand Up @@ -1247,6 +1250,9 @@ Instance methods:
``tzinfo=None`` can be specified to create a naive datetime from an aware
datetime with no conversion of date and time data.

:class:`datetime` objects are also supported by generic function
:func:`copy.replace`.

.. versionadded:: 3.6
Added the ``fold`` argument.

Expand Down Expand Up @@ -1823,6 +1829,9 @@ Instance methods:
``tzinfo=None`` can be specified to create a naive :class:`.time` from an
aware :class:`.time`, without conversion of the time data.

:class:`time` objects are also supported by generic function
:func:`copy.replace`.

.. versionadded:: 3.6
Added the ``fold`` argument.

Expand Down
11 changes: 8 additions & 3 deletions Doc/library/inspect.rst
Original file line number Diff line number Diff line change
Expand Up @@ -689,8 +689,8 @@ function.
The optional *return_annotation* argument, can be an arbitrary Python object,
is the "return" annotation of the callable.

Signature objects are *immutable*. Use :meth:`Signature.replace` to make a
modified copy.
Signature objects are *immutable*. Use :meth:`Signature.replace` or
:func:`copy.replace` to make a modified copy.

.. versionchanged:: 3.5
Signature objects are picklable and :term:`hashable`.
Expand Down Expand Up @@ -746,6 +746,9 @@ function.
>>> str(new_sig)
"(a, b) -> 'new return anno'"

Signature objects are also supported by generic function
:func:`copy.replace`.

.. classmethod:: Signature.from_callable(obj, *, follow_wrapped=True, globalns=None, localns=None)

Return a :class:`Signature` (or its subclass) object for a given callable
Expand All @@ -769,7 +772,7 @@ function.
.. class:: Parameter(name, kind, *, default=Parameter.empty, annotation=Parameter.empty)

Parameter objects are *immutable*. Instead of modifying a Parameter object,
you can use :meth:`Parameter.replace` to create a modified copy.
you can use :meth:`Parameter.replace` or :func:`copy.replace` to create a modified copy.

.. versionchanged:: 3.5
Parameter objects are picklable and :term:`hashable`.
Expand Down Expand Up @@ -892,6 +895,8 @@ function.
>>> str(param.replace(default=Parameter.empty, annotation='spam'))
"foo:'spam'"

Parameter objects are also supported by generic function :func:`copy.replace`.

.. versionchanged:: 3.4
In Python 3.3 Parameter objects were allowed to have ``name`` set
to ``None`` if their ``kind`` was set to ``POSITIONAL_ONLY``.
Expand Down
2 changes: 2 additions & 0 deletions Doc/library/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ Standard names are defined for the following types:

Return a copy of the code object with new values for the specified fields.

Code objects are also supported by generic function :func:`copy.replace`.

.. versionadded:: 3.8

.. data:: CellType
Expand Down
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ array
It can be used instead of ``'u'`` type code, which is deprecated.
(Contributed by Inada Naoki in :gh:`80480`.)

copy
----

* Add :func:`copy.replace` function which allows to create a modified copy of
an object, which is especially usefule for immutable objects.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usefule -> useful

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Do you mind to create a PR to fix this typo?

It supports named tuples created with the factory function
:func:`collections.namedtuple`, :class:`~dataclasses.dataclass` instances,
various :mod:`datetime` objects, :class:`~inspect.Signature` objects,
:class:`~inspect.Parameter` objects, :ref:`code object <code-objects>`, and
any user classes which define the :meth:`!__replace__` method.
(Contributed by Serhiy Storchaka in :gh:`108751`.)

dbm
---

Expand Down
6 changes: 6 additions & 0 deletions Lib/_pydatetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,8 @@ def replace(self, year=None, month=None, day=None):
day = self._day
return type(self)(year, month, day)

__replace__ = replace

# Comparisons of date objects with other.

def __eq__(self, other):
Expand Down Expand Up @@ -1637,6 +1639,8 @@ def replace(self, hour=None, minute=None, second=None, microsecond=None,
fold = self._fold
return type(self)(hour, minute, second, microsecond, tzinfo, fold=fold)

__replace__ = replace

# Pickle support.

def _getstate(self, protocol=3):
Expand Down Expand Up @@ -1983,6 +1987,8 @@ def replace(self, year=None, month=None, day=None, hour=None,
return type(self)(year, month, day, hour, minute, second,
microsecond, tzinfo, fold=fold)

__replace__ = replace

def _local_timezone(self):
if self.tzinfo is None:
ts = self._mktime()
Expand Down
1 change: 1 addition & 0 deletions Lib/collections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ def __getnewargs__(self):
'_field_defaults': field_defaults,
'__new__': __new__,
'_make': _make,
'__replace__': _replace,
'_replace': _replace,
'__repr__': __repr__,
'_asdict': _asdict,
Expand Down
13 changes: 13 additions & 0 deletions Lib/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,16 @@ def _reconstruct(x, memo, func, args,
return y

del types, weakref


def replace(obj, /, **changes):
"""Return a new object replacing specified fields with new values.

This is especially useful for immutable objects, like named tuples or
frozen dataclasses.
"""
cls = obj.__class__
func = getattr(cls, '__replace__', None)
if func is None:
raise TypeError(f"replace() does not support {cls.__name__} objects")
return func(obj, **changes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a cool feature for very little code!

9 changes: 6 additions & 3 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
globals,
slots,
))
_set_new_attribute(cls, '__replace__', _replace)

# Get the fields as a list, and include only real fields. This is
# used in all of the following methods.
Expand Down Expand Up @@ -1546,13 +1547,15 @@ class C:
c1 = replace(c, x=3)
assert c1.x == 3 and c1.y == 2
"""
if not _is_dataclass_instance(obj):
raise TypeError("replace() should be called on dataclass instances")
return _replace(obj, **changes)


def _replace(obj, /, **changes):
# We're going to mutate 'changes', but that's okay because it's a
# new dict, even if called with 'replace(obj, **my_changes)'.

if not _is_dataclass_instance(obj):
raise TypeError("replace() should be called on dataclass instances")

# It's an error to have init=False fields in 'changes'.
# If a field is not in 'changes', read its value from the provided obj.

Expand Down
4 changes: 4 additions & 0 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -2870,6 +2870,8 @@ def __str__(self):

return formatted

__replace__ = replace

def __repr__(self):
return '<{} "{}">'.format(self.__class__.__name__, self)

Expand Down Expand Up @@ -3130,6 +3132,8 @@ def replace(self, *, parameters=_void, return_annotation=_void):
return type(self)(parameters,
return_annotation=return_annotation)

__replace__ = replace

def _hash_basis(self):
params = tuple(param for param in self.parameters.values()
if param.kind != _KEYWORD_ONLY)
Expand Down
Loading