]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-155041: Support copy.replace() for decimal.Context (GH-155047)
authorSerhiy Storchaka <storchaka@gmail.com>
Sun, 2 Aug 2026 06:26:40 +0000 (09:26 +0300)
committerGitHub <noreply@github.com>
Sun, 2 Aug 2026 06:26:40 +0000 (09:26 +0300)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Doc/library/decimal.rst
Lib/_pydecimal.py
Lib/test/test_decimal.py
Misc/NEWS.d/next/Library/2026-08-01-19-02-00.gh-issue-155041.Dc1Ctx.rst [new file with mode: 0644]
Modules/_decimal/_decimal.c
Modules/_decimal/clinic/_decimal.c.h

index 2af5dfce9612b373030d582dfee3cbfd82a0e32e..ecd973a070b3dffd7c7d318596e9d4f2ba99063e 100644 (file)
@@ -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.
index 8c0afd14d616e85fc2952924090e8650aa185d88..e0b32b7a5e21139f9d42ff77002c89d23a1d65a3 100644 (file)
@@ -4005,6 +4005,20 @@ class Context(object):
         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
 
index 1c723b25784da11dcd3f14f4a8cf2b2d7db80f33..493732c22e8231ab675dc0797ae82bba84f318c7 100644 (file)
@@ -3070,6 +3070,41 @@ class ContextAPItests:
         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 (file)
index 0000000..237626e
--- /dev/null
@@ -0,0 +1 @@
+:class:`decimal.Context` objects now support :func:`copy.replace`.
index ac3ffb23248f342025a0f00bc7153f6db4a5be80..38944e6a9d9017c013c0c61bd4874b03aeb36833 100644 (file)
@@ -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
index c803006ad443825433adf8b61da49459d94f3a35..7c45bc66569bb53406d223e4429ae33b4ed41e5f 100644 (file)
@@ -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 @@ exit:
 #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]*/