From f6c6a0fc97b7c3c754e0379241a3e19b9633d68c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 1 Aug 2026 22:24:25 +0300 Subject: [PATCH] gh-155051: Reject None keyword arguments in decimal.localcontext() None is not a valid value for any of the context attributes. The C implementation silently ignored it, because it shared the code with the Context constructor, where None means "not specified". The pure Python implementation always rejected it. Co-Authored-By: Claude Opus 5 (1M context) --- Lib/test/test_decimal.py | 7 +++ ...-08-01-21-00-00.gh-issue-155051.Lc1Non.rst | 3 ++ Modules/_decimal/_decimal.c | 46 +++++++++++-------- Modules/_decimal/clinic/_decimal.c.h | 18 ++++---- 4 files changed, 45 insertions(+), 29 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-01-21-00-00.gh-issue-155051.Lc1Non.rst diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 1c723b25784da11..269dc7dae65777a 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -3763,6 +3763,13 @@ def test_localcontext_kwargs(self): self.assertRaises(TypeError, self.decimal.localcontext, Emin="") self.assertRaises(TypeError, self.decimal.localcontext, Emax="") + # None is not a valid value for any of these attributes. + for name in ('prec', 'rounding', 'Emin', 'Emax', 'capitals', 'clamp', + 'flags', 'traps'): + with self.subTest(name=name): + self.assertRaises(TypeError, self.decimal.localcontext, + **{name: None}) + def test_local_context_kwargs_does_not_overwrite_existing_argument(self): ctx = self.decimal.getcontext() orig_prec = ctx.prec diff --git a/Misc/NEWS.d/next/Library/2026-08-01-21-00-00.gh-issue-155051.Lc1Non.rst b/Misc/NEWS.d/next/Library/2026-08-01-21-00-00.gh-issue-155051.Lc1Non.rst new file mode 100644 index 000000000000000..d2be40f243d2969 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-01-21-00-00.gh-issue-155051.Lc1Non.rst @@ -0,0 +1,3 @@ +:func:`decimal.localcontext` now raises :exc:`TypeError` if a keyword argument +is ``None``, as the pure Python implementation already did. Previously the C +implementation silently ignored it. diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index ac3ffb23248f342..65380eb32e877a8 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 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) ); } @@ -2052,14 +2058,14 @@ _decimal.localcontext ctx as local: object = None * - prec: object = None - rounding: object = None - Emin: object = None - Emax: object = None - capitals: object = None - clamp: object = None - flags: object = None - traps: object = None + prec: object = NULL + rounding: object = NULL + Emin: object = NULL + Emax: object = NULL + capitals: object = NULL + clamp: object = NULL + flags: object = NULL + traps: object = NULL Return a context manager for a copy of the supplied context. @@ -2074,7 +2080,7 @@ _decimal_localcontext_impl(PyObject *module, PyObject *local, PyObject *prec, PyObject *rounding, PyObject *Emin, PyObject *Emax, PyObject *capitals, PyObject *clamp, PyObject *flags, PyObject *traps) -/*[clinic end generated code: output=9bf4e47742a809b0 input=490307b9689c3856]*/ +/*[clinic end generated code: output=9bf4e47742a809b0 input=616abb6ee1654373]*/ { PyObject *global; diff --git a/Modules/_decimal/clinic/_decimal.c.h b/Modules/_decimal/clinic/_decimal.c.h index c803006ad443825..4e82a7750c51915 100644 --- a/Modules/_decimal/clinic/_decimal.c.h +++ b/Modules/_decimal/clinic/_decimal.c.h @@ -512,14 +512,14 @@ _decimal_localcontext(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *argsbuf[9]; Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; PyObject *local = Py_None; - PyObject *prec = Py_None; - PyObject *rounding = Py_None; - PyObject *Emin = Py_None; - PyObject *Emax = Py_None; - PyObject *capitals = Py_None; - PyObject *clamp = Py_None; - PyObject *flags = Py_None; - PyObject *traps = Py_None; + PyObject *prec = NULL; + PyObject *rounding = NULL; + PyObject *Emin = NULL; + PyObject *Emax = NULL; + PyObject *capitals = NULL; + PyObject *clamp = NULL; + PyObject *flags = NULL; + PyObject *traps = NULL; args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 0, /*maxpos*/ 1, /*minkw*/ 0, /*varpos*/ 0, argsbuf); @@ -6984,4 +6984,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=1928815cb039f2a2 input=a9049054013a1b77]*/