Skip to content
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
8 changes: 8 additions & 0 deletions Doc/library/decimal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,14 @@ In addition to the three supplied contexts, new contexts can be created with the

Return a duplicate of the context.

:class:`!Context` objects also support :func:`copy.replace`,
which returns a duplicate with the specified fields replaced.
Fields which are not specified keep the values
they have in the original context.

.. versionchanged:: next
Added support for :func:`copy.replace`.

.. method:: copy_decimal(num, /)

Return a copy of the Decimal instance num.
Expand Down
14 changes: 14 additions & 0 deletions Lib/_pydecimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4005,6 +4005,20 @@ def copy(self):
return nc
__copy__ = copy

def __replace__(self, /, **changes):
"""Returns a copy of self with the specified attributes replaced."""
unexpected = changes.keys() - _context_attributes
if unexpected:
raise TypeError(f'__replace__() got an unexpected keyword '
f'argument {min(unexpected)!r}')
nc = self.copy()
for name, value in changes.items():
if name in ('flags', 'traps') and isinstance(value, list):
# As in the constructor, accept a list of signals.
value = dict((s, int(s in value)) for s in _signals + value)
setattr(nc, name, value)
return nc

def _raise_error(self, condition, explanation = None, *args):
"""Handles an error

Expand Down
35 changes: 35 additions & 0 deletions Lib/test/test_decimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3070,6 +3070,41 @@ def test_copy(self):
self.assertEqual(k1, k2)
self.assertEqual(c.flags, d.flags)

def test_replace(self):
Context = self.decimal.Context
Inexact = self.decimal.Inexact
Overflow = self.decimal.Overflow
ROUND_UP = self.decimal.ROUND_UP

c = Context(prec=10, Emin=-99, capitals=0)
c.flags[Inexact] = True
d = copy.replace(c, prec=20, rounding=ROUND_UP)
self.assertEqual(d.prec, 20)
self.assertEqual(d.rounding, ROUND_UP)
# Not replaced attributes are inherited from the original context.
self.assertEqual(d.Emin, -99)
self.assertEqual(d.capitals, 0)
self.assertEqual(d.Emax, c.Emax)
self.assertEqual(d.clamp, c.clamp)
self.assertTrue(d.flags[Inexact])
self.assertEqual(d.traps, c.traps)
# The copy is deep and the original context is left unchanged.
self.assertIsNot(d.flags, c.flags)
self.assertIsNot(d.traps, c.traps)
self.assertEqual(c.prec, 10)
self.assertEqual(c.rounding, Context().rounding)

# As in the constructor, flags and traps can be given as a list.
d = copy.replace(c, flags=[Overflow])
self.assertTrue(d.flags[Overflow])
self.assertFalse(d.flags[Inexact])

self.assertRaises(TypeError, copy.replace, c, prek=1)
self.assertRaises(TypeError, copy.replace, c, prec='spam')
# Unlike in the constructor, None is not a valid value.
self.assertRaises(TypeError, copy.replace, c, prec=None)
self.assertRaises(TypeError, copy.replace, c, flags=None)

def test__clamp(self):
# In Python 3.2, the private attribute `_clamp` was made
# public (issue 8540), with the old `_clamp` becoming a
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:class:`decimal.Context` objects now support :func:`copy.replace`.
76 changes: 62 additions & 14 deletions Modules/_decimal/_decimal.c
Original file line number Diff line number Diff line change
Expand Up @@ -1333,32 +1333,37 @@ context_setattr(PyObject *self, PyObject *name, PyObject *value)
return PyObject_GenericSetAttr(self, name, value);
}

/* In the constructor and in localcontext() None means "not specified". */
#define NONE_TO_NULL(x) ((x) == Py_None ? NULL : (x))

/* Set the given attributes. An attribute is left unchanged if the
corresponding argument is NULL. */
static int
context_setattrs(PyObject *self, PyObject *prec, PyObject *rounding,
PyObject *emin, PyObject *emax, PyObject *capitals,
PyObject *clamp, PyObject *status, PyObject *traps) {

int ret;
if (prec != Py_None && context_setprec(self, prec, NULL) < 0) {
if (prec != NULL && context_setprec(self, prec, NULL) < 0) {
return -1;
}
if (rounding != Py_None && context_setround(self, rounding, NULL) < 0) {
if (rounding != NULL && context_setround(self, rounding, NULL) < 0) {
return -1;
}
if (emin != Py_None && context_setemin(self, emin, NULL) < 0) {
if (emin != NULL && context_setemin(self, emin, NULL) < 0) {
return -1;
}
if (emax != Py_None && context_setemax(self, emax, NULL) < 0) {
if (emax != NULL && context_setemax(self, emax, NULL) < 0) {
return -1;
}
if (capitals != Py_None && context_setcapitals(self, capitals, NULL) < 0) {
if (capitals != NULL && context_setcapitals(self, capitals, NULL) < 0) {
return -1;
}
if (clamp != Py_None && context_setclamp(self, clamp, NULL) < 0) {
if (clamp != NULL && context_setclamp(self, clamp, NULL) < 0) {
return -1;
}

if (traps != Py_None) {
if (traps != NULL) {
if (PyList_Check(traps)) {
ret = context_settraps_list(self, traps);
}
Expand All @@ -1374,7 +1379,7 @@ context_setattrs(PyObject *self, PyObject *prec, PyObject *rounding,
return ret;
}
}
if (status != Py_None) {
if (status != NULL) {
if (PyList_Check(status)) {
ret = context_setstatus_list(self, status);
}
Expand Down Expand Up @@ -1559,10 +1564,11 @@ context_init_impl(PyObject *self, PyObject *prec, PyObject *rounding,
PyObject *clamp, PyObject *status, PyObject *traps)
/*[clinic end generated code: output=8bfdc59fbe862f44 input=45c704b93cd02959]*/
{
/* The context has already been initialized with the default values. */
return context_setattrs(
self, prec, rounding,
emin, emax, capitals,
clamp, status, traps
self, NONE_TO_NULL(prec), NONE_TO_NULL(rounding),
NONE_TO_NULL(emin), NONE_TO_NULL(emax), NONE_TO_NULL(capitals),
NONE_TO_NULL(clamp), NONE_TO_NULL(status), NONE_TO_NULL(traps)
);
}

Expand Down Expand Up @@ -1716,6 +1722,47 @@ _decimal_Context___copy___impl(PyObject *self, PyTypeObject *cls)
return context_copy(state, self);
}

/*[clinic input]
@text_signature "($self, /, **changes)"
_decimal.Context.__replace__

cls: defining_class
*
prec: object = NULL
rounding: object = NULL
Emin as emin: object = NULL
Emax as emax: object = NULL
capitals: object = NULL
clamp: object = NULL
flags as status: object = NULL
traps: object = NULL
Comment on lines +1731 to +1738

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The problem with NULL defaults, is that this not supported by inspect.signature(), rendered as "unrepresentable".

But perhaps it for another day, we have other examples in the decimal module. Using a sentinel might be an option.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is why we use @text_signature "($self, /, **changes)" like for all other __replace__ methods.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is a workaround, yes. Hence, PR was approved.

But I think that explicit arguments would be better.


Return a copy of the context with the specified attributes replaced.
[clinic start generated code]*/

static PyObject *
_decimal_Context___replace___impl(PyObject *self, PyTypeObject *cls,
PyObject *prec, PyObject *rounding,
PyObject *emin, PyObject *emax,
PyObject *capitals, PyObject *clamp,
PyObject *status, PyObject *traps)
/*[clinic end generated code: output=375ef0392df682ec input=414d9d00b25af6f3]*/
{
decimal_state *state = PyType_GetModuleState(cls);

PyObject *result = context_copy(state, self);
if (result == NULL) {
return NULL;
}
if (context_setattrs(result, prec, rounding, emin, emax, capitals,
clamp, status, traps) < 0)
{
Py_DECREF(result);
return NULL;
}
return result;
}

/*[clinic input]
_decimal.Context.__reduce__ = _decimal.Context.copy

Expand Down Expand Up @@ -2095,9 +2142,9 @@ _decimal_localcontext_impl(PyObject *module, PyObject *local, PyObject *prec,
}

int ret = context_setattrs(
local_copy, prec, rounding,
Emin, Emax, capitals,
clamp, flags, traps
local_copy, NONE_TO_NULL(prec), NONE_TO_NULL(rounding),
NONE_TO_NULL(Emin), NONE_TO_NULL(Emax), NONE_TO_NULL(capitals),
NONE_TO_NULL(clamp), NONE_TO_NULL(flags), NONE_TO_NULL(traps)
);
if (ret < 0) {
Py_DECREF(local_copy);
Expand Down Expand Up @@ -7557,6 +7604,7 @@ static PyMethodDef context_methods [] =

/* Miscellaneous */
_DECIMAL_CONTEXT___COPY___METHODDEF
_DECIMAL_CONTEXT___REPLACE___METHODDEF
_DECIMAL_CONTEXT___REDUCE___METHODDEF
_DECIMAL_CONTEXT_COPY_METHODDEF
_DECIMAL_CONTEXT_CREATE_DECIMAL_METHODDEF
Expand Down
118 changes: 117 additions & 1 deletion Modules/_decimal/clinic/_decimal.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading