Skip to content

Commit 3f4ed1d

Browse files
gh-155041: Support copy.replace() for decimal.Context (GH-155047)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 7b4165b commit 3f4ed1d

6 files changed

Lines changed: 237 additions & 15 deletions

File tree

Doc/library/decimal.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,14 @@ In addition to the three supplied contexts, new contexts can be created with the
11821182

11831183
Return a duplicate of the context.
11841184

1185+
:class:`!Context` objects also support :func:`copy.replace`,
1186+
which returns a duplicate with the specified fields replaced.
1187+
Fields which are not specified keep the values
1188+
they have in the original context.
1189+
1190+
.. versionchanged:: next
1191+
Added support for :func:`copy.replace`.
1192+
11851193
.. method:: copy_decimal(num, /)
11861194

11871195
Return a copy of the Decimal instance num.

Lib/_pydecimal.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4005,6 +4005,20 @@ def copy(self):
40054005
return nc
40064006
__copy__ = copy
40074007

4008+
def __replace__(self, /, **changes):
4009+
"""Returns a copy of self with the specified attributes replaced."""
4010+
unexpected = changes.keys() - _context_attributes
4011+
if unexpected:
4012+
raise TypeError(f'__replace__() got an unexpected keyword '
4013+
f'argument {min(unexpected)!r}')
4014+
nc = self.copy()
4015+
for name, value in changes.items():
4016+
if name in ('flags', 'traps') and isinstance(value, list):
4017+
# As in the constructor, accept a list of signals.
4018+
value = dict((s, int(s in value)) for s in _signals + value)
4019+
setattr(nc, name, value)
4020+
return nc
4021+
40084022
def _raise_error(self, condition, explanation = None, *args):
40094023
"""Handles an error
40104024

Lib/test/test_decimal.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3070,6 +3070,41 @@ def test_copy(self):
30703070
self.assertEqual(k1, k2)
30713071
self.assertEqual(c.flags, d.flags)
30723072

3073+
def test_replace(self):
3074+
Context = self.decimal.Context
3075+
Inexact = self.decimal.Inexact
3076+
Overflow = self.decimal.Overflow
3077+
ROUND_UP = self.decimal.ROUND_UP
3078+
3079+
c = Context(prec=10, Emin=-99, capitals=0)
3080+
c.flags[Inexact] = True
3081+
d = copy.replace(c, prec=20, rounding=ROUND_UP)
3082+
self.assertEqual(d.prec, 20)
3083+
self.assertEqual(d.rounding, ROUND_UP)
3084+
# Not replaced attributes are inherited from the original context.
3085+
self.assertEqual(d.Emin, -99)
3086+
self.assertEqual(d.capitals, 0)
3087+
self.assertEqual(d.Emax, c.Emax)
3088+
self.assertEqual(d.clamp, c.clamp)
3089+
self.assertTrue(d.flags[Inexact])
3090+
self.assertEqual(d.traps, c.traps)
3091+
# The copy is deep and the original context is left unchanged.
3092+
self.assertIsNot(d.flags, c.flags)
3093+
self.assertIsNot(d.traps, c.traps)
3094+
self.assertEqual(c.prec, 10)
3095+
self.assertEqual(c.rounding, Context().rounding)
3096+
3097+
# As in the constructor, flags and traps can be given as a list.
3098+
d = copy.replace(c, flags=[Overflow])
3099+
self.assertTrue(d.flags[Overflow])
3100+
self.assertFalse(d.flags[Inexact])
3101+
3102+
self.assertRaises(TypeError, copy.replace, c, prek=1)
3103+
self.assertRaises(TypeError, copy.replace, c, prec='spam')
3104+
# Unlike in the constructor, None is not a valid value.
3105+
self.assertRaises(TypeError, copy.replace, c, prec=None)
3106+
self.assertRaises(TypeError, copy.replace, c, flags=None)
3107+
30733108
def test__clamp(self):
30743109
# In Python 3.2, the private attribute `_clamp` was made
30753110
# public (issue 8540), with the old `_clamp` becoming a
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:class:`decimal.Context` objects now support :func:`copy.replace`.

Modules/_decimal/_decimal.c

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1333,32 +1333,37 @@ context_setattr(PyObject *self, PyObject *name, PyObject *value)
13331333
return PyObject_GenericSetAttr(self, name, value);
13341334
}
13351335

1336+
/* In the constructor and in localcontext() None means "not specified". */
1337+
#define NONE_TO_NULL(x) ((x) == Py_None ? NULL : (x))
1338+
1339+
/* Set the given attributes. An attribute is left unchanged if the
1340+
corresponding argument is NULL. */
13361341
static int
13371342
context_setattrs(PyObject *self, PyObject *prec, PyObject *rounding,
13381343
PyObject *emin, PyObject *emax, PyObject *capitals,
13391344
PyObject *clamp, PyObject *status, PyObject *traps) {
13401345

13411346
int ret;
1342-
if (prec != Py_None && context_setprec(self, prec, NULL) < 0) {
1347+
if (prec != NULL && context_setprec(self, prec, NULL) < 0) {
13431348
return -1;
13441349
}
1345-
if (rounding != Py_None && context_setround(self, rounding, NULL) < 0) {
1350+
if (rounding != NULL && context_setround(self, rounding, NULL) < 0) {
13461351
return -1;
13471352
}
1348-
if (emin != Py_None && context_setemin(self, emin, NULL) < 0) {
1353+
if (emin != NULL && context_setemin(self, emin, NULL) < 0) {
13491354
return -1;
13501355
}
1351-
if (emax != Py_None && context_setemax(self, emax, NULL) < 0) {
1356+
if (emax != NULL && context_setemax(self, emax, NULL) < 0) {
13521357
return -1;
13531358
}
1354-
if (capitals != Py_None && context_setcapitals(self, capitals, NULL) < 0) {
1359+
if (capitals != NULL && context_setcapitals(self, capitals, NULL) < 0) {
13551360
return -1;
13561361
}
1357-
if (clamp != Py_None && context_setclamp(self, clamp, NULL) < 0) {
1362+
if (clamp != NULL && context_setclamp(self, clamp, NULL) < 0) {
13581363
return -1;
13591364
}
13601365

1361-
if (traps != Py_None) {
1366+
if (traps != NULL) {
13621367
if (PyList_Check(traps)) {
13631368
ret = context_settraps_list(self, traps);
13641369
}
@@ -1374,7 +1379,7 @@ context_setattrs(PyObject *self, PyObject *prec, PyObject *rounding,
13741379
return ret;
13751380
}
13761381
}
1377-
if (status != Py_None) {
1382+
if (status != NULL) {
13781383
if (PyList_Check(status)) {
13791384
ret = context_setstatus_list(self, status);
13801385
}
@@ -1559,10 +1564,11 @@ context_init_impl(PyObject *self, PyObject *prec, PyObject *rounding,
15591564
PyObject *clamp, PyObject *status, PyObject *traps)
15601565
/*[clinic end generated code: output=8bfdc59fbe862f44 input=45c704b93cd02959]*/
15611566
{
1567+
/* The context has already been initialized with the default values. */
15621568
return context_setattrs(
1563-
self, prec, rounding,
1564-
emin, emax, capitals,
1565-
clamp, status, traps
1569+
self, NONE_TO_NULL(prec), NONE_TO_NULL(rounding),
1570+
NONE_TO_NULL(emin), NONE_TO_NULL(emax), NONE_TO_NULL(capitals),
1571+
NONE_TO_NULL(clamp), NONE_TO_NULL(status), NONE_TO_NULL(traps)
15661572
);
15671573
}
15681574

@@ -1716,6 +1722,47 @@ _decimal_Context___copy___impl(PyObject *self, PyTypeObject *cls)
17161722
return context_copy(state, self);
17171723
}
17181724

1725+
/*[clinic input]
1726+
@text_signature "($self, /, **changes)"
1727+
_decimal.Context.__replace__
1728+
1729+
cls: defining_class
1730+
*
1731+
prec: object = NULL
1732+
rounding: object = NULL
1733+
Emin as emin: object = NULL
1734+
Emax as emax: object = NULL
1735+
capitals: object = NULL
1736+
clamp: object = NULL
1737+
flags as status: object = NULL
1738+
traps: object = NULL
1739+
1740+
Return a copy of the context with the specified attributes replaced.
1741+
[clinic start generated code]*/
1742+
1743+
static PyObject *
1744+
_decimal_Context___replace___impl(PyObject *self, PyTypeObject *cls,
1745+
PyObject *prec, PyObject *rounding,
1746+
PyObject *emin, PyObject *emax,
1747+
PyObject *capitals, PyObject *clamp,
1748+
PyObject *status, PyObject *traps)
1749+
/*[clinic end generated code: output=375ef0392df682ec input=414d9d00b25af6f3]*/
1750+
{
1751+
decimal_state *state = PyType_GetModuleState(cls);
1752+
1753+
PyObject *result = context_copy(state, self);
1754+
if (result == NULL) {
1755+
return NULL;
1756+
}
1757+
if (context_setattrs(result, prec, rounding, emin, emax, capitals,
1758+
clamp, status, traps) < 0)
1759+
{
1760+
Py_DECREF(result);
1761+
return NULL;
1762+
}
1763+
return result;
1764+
}
1765+
17191766
/*[clinic input]
17201767
_decimal.Context.__reduce__ = _decimal.Context.copy
17211768
@@ -2095,9 +2142,9 @@ _decimal_localcontext_impl(PyObject *module, PyObject *local, PyObject *prec,
20952142
}
20962143

20972144
int ret = context_setattrs(
2098-
local_copy, prec, rounding,
2099-
Emin, Emax, capitals,
2100-
clamp, flags, traps
2145+
local_copy, NONE_TO_NULL(prec), NONE_TO_NULL(rounding),
2146+
NONE_TO_NULL(Emin), NONE_TO_NULL(Emax), NONE_TO_NULL(capitals),
2147+
NONE_TO_NULL(clamp), NONE_TO_NULL(flags), NONE_TO_NULL(traps)
21012148
);
21022149
if (ret < 0) {
21032150
Py_DECREF(local_copy);
@@ -7557,6 +7604,7 @@ static PyMethodDef context_methods [] =
75577604

75587605
/* Miscellaneous */
75597606
_DECIMAL_CONTEXT___COPY___METHODDEF
7607+
_DECIMAL_CONTEXT___REPLACE___METHODDEF
75607608
_DECIMAL_CONTEXT___REDUCE___METHODDEF
75617609
_DECIMAL_CONTEXT_COPY_METHODDEF
75627610
_DECIMAL_CONTEXT_CREATE_DECIMAL_METHODDEF

Modules/_decimal/clinic/_decimal.c.h

Lines changed: 117 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)