diff --git a/Doc/library/decimal.rst b/Doc/library/decimal.rst index 2af5dfce9612b37..ecd973a070b3dff 100644 --- a/Doc/library/decimal.rst +++ b/Doc/library/decimal.rst @@ -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. diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 8c0afd14d616e85..e0b32b7a5e21139 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -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 diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 1c723b25784da11..493732c22e8231a 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -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 diff --git a/Misc/NEWS.d/next/Library/2026-08-01-19-02-00.gh-issue-155041.Dc1Ctx.rst b/Misc/NEWS.d/next/Library/2026-08-01-19-02-00.gh-issue-155041.Dc1Ctx.rst new file mode 100644 index 000000000000000..237626e0fbc5397 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-01-19-02-00.gh-issue-155041.Dc1Ctx.rst @@ -0,0 +1 @@ +:class:`decimal.Context` objects now support :func:`copy.replace`. diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index ac3ffb23248f342..38944e6a9d9017c 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -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); } @@ -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); } @@ -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) ); } @@ -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 + +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 @@ -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); @@ -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 diff --git a/Modules/_decimal/clinic/_decimal.c.h b/Modules/_decimal/clinic/_decimal.c.h index c803006ad443825..7c45bc66569bb53 100644 --- a/Modules/_decimal/clinic/_decimal.c.h +++ b/Modules/_decimal/clinic/_decimal.c.h @@ -410,6 +410,122 @@ _decimal_Context___copy__(PyObject *self, PyTypeObject *cls, PyObject *const *ar return _decimal_Context___copy___impl(self, cls); } +PyDoc_STRVAR(_decimal_Context___replace____doc__, +"__replace__($self, /, **changes)\n" +"--\n" +"\n" +"Return a copy of the context with the specified attributes replaced."); + +#define _DECIMAL_CONTEXT___REPLACE___METHODDEF \ + {"__replace__", _PyCFunction_CAST(_decimal_Context___replace__), METH_METHOD|METH_FASTCALL|METH_KEYWORDS, _decimal_Context___replace____doc__}, + +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); + +static PyObject * +_decimal_Context___replace__(PyObject *self, PyTypeObject *cls, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 8 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(prec), &_Py_ID(rounding), &_Py_ID(Emin), &_Py_ID(Emax), &_Py_ID(capitals), &_Py_ID(clamp), &_Py_ID(flags), &_Py_ID(traps), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"prec", "rounding", "Emin", "Emax", "capitals", "clamp", "flags", "traps", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "__replace__", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[8]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; + PyObject *prec = NULL; + PyObject *rounding = NULL; + PyObject *emin = NULL; + PyObject *emax = NULL; + PyObject *capitals = NULL; + PyObject *clamp = NULL; + PyObject *status = NULL; + PyObject *traps = NULL; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + if (args[0]) { + prec = args[0]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (args[1]) { + rounding = args[1]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (args[2]) { + emin = args[2]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (args[3]) { + emax = args[3]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (args[4]) { + capitals = args[4]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (args[5]) { + clamp = args[5]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + if (args[6]) { + status = args[6]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + traps = args[7]; +skip_optional_kwonly: + return_value = _decimal_Context___replace___impl(self, cls, prec, rounding, emin, emax, capitals, clamp, status, traps); + +exit: + return return_value; +} + PyDoc_STRVAR(_decimal_Context___reduce____doc__, "__reduce__($self, /)\n" "--\n" @@ -6984,4 +7100,4 @@ _decimal_Context_same_quantum(PyObject *context, PyTypeObject *cls, PyObject *co #ifndef _DECIMAL_CONTEXT_APPLY_METHODDEF #define _DECIMAL_CONTEXT_APPLY_METHODDEF #endif /* !defined(_DECIMAL_CONTEXT_APPLY_METHODDEF) */ -/*[clinic end generated code: output=0eb835634388294e input=a9049054013a1b77]*/ +/*[clinic end generated code: output=75ab23467fa0eee3 input=a9049054013a1b77]*/