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
6 changes: 5 additions & 1 deletion Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1709,7 +1709,11 @@ def removeHandler(self, hdlr):
"""
with _lock:
if hdlr in self.handlers:
self.handlers.remove(hdlr)
# Replace the list instead of mutating it in place, so that
# callHandlers() can iterate it without a lock (gh-79366).
handlers = self.handlers.copy()
handlers.remove(hdlr)
self.handlers = handlers

def hasHandlers(self):
"""
Expand Down
42 changes: 42 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 Expand Up @@ -3763,6 +3798,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
Expand Down
17 changes: 17 additions & 0 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,23 @@ def lock_holder_thread_fn():

support.wait_process(pid, exitcode=0)

def test_remove_handler_while_emitting(self):
# Removing a handler while callHandlers() iterates over the handlers
# should not cause the following handlers to be skipped (gh-79366).
logger = logging.Logger('test_remove_handler_while_emitting')
calls = []
class RemovingHandler(logging.Handler):
def emit(self, record):
calls.append('removing')
logger.removeHandler(self)
class CountingHandler(logging.Handler):
def emit(self, record):
calls.append('counting')
logger.addHandler(RemovingHandler())
logger.addHandler(CountingHandler())
logger.error('spam')
self.assertEqual(calls, ['removing', 'counting'])


class BadStream(object):
def write(self, data):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixed a race condition in :mod:`logging`:
if a handler was removed while a record was being emitted,
the following handlers of the same logger could be skipped.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:class:`decimal.Context` objects now support :func:`copy.replace`.
Original file line number Diff line number Diff line change
@@ -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.
88 changes: 68 additions & 20 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 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

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 @@ -2052,14 +2099,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.

Expand All @@ -2074,7 +2121,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;

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
Loading
Loading