Skip to content

Commit 4e3560f

Browse files
Merge clean grammar changes into features
2 parents 136490f + 97998dd commit 4e3560f

13 files changed

Lines changed: 307 additions & 36 deletions

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/logging/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1709,7 +1709,11 @@ def removeHandler(self, hdlr):
17091709
"""
17101710
with _lock:
17111711
if hdlr in self.handlers:
1712-
self.handlers.remove(hdlr)
1712+
# Replace the list instead of mutating it in place, so that
1713+
# callHandlers() can iterate it without a lock (gh-79366).
1714+
handlers = self.handlers.copy()
1715+
handlers.remove(hdlr)
1716+
self.handlers = handlers
17131717

17141718
def hasHandlers(self):
17151719
"""

Lib/test/test_decimal.py

Lines changed: 42 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
@@ -3763,6 +3798,13 @@ def test_localcontext_kwargs(self):
37633798
self.assertRaises(TypeError, self.decimal.localcontext, Emin="")
37643799
self.assertRaises(TypeError, self.decimal.localcontext, Emax="")
37653800

3801+
# None is not a valid value for any of these attributes.
3802+
for name in ('prec', 'rounding', 'Emin', 'Emax', 'capitals', 'clamp',
3803+
'flags', 'traps'):
3804+
with self.subTest(name=name):
3805+
self.assertRaises(TypeError, self.decimal.localcontext,
3806+
**{name: None})
3807+
37663808
def test_local_context_kwargs_does_not_overwrite_existing_argument(self):
37673809
ctx = self.decimal.getcontext()
37683810
orig_prec = ctx.prec

Lib/test/test_logging.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,23 @@ def lock_holder_thread_fn():
814814

815815
support.wait_process(pid, exitcode=0)
816816

817+
def test_remove_handler_while_emitting(self):
818+
# Removing a handler while callHandlers() iterates over the handlers
819+
# should not cause the following handlers to be skipped (gh-79366).
820+
logger = logging.Logger('test_remove_handler_while_emitting')
821+
calls = []
822+
class RemovingHandler(logging.Handler):
823+
def emit(self, record):
824+
calls.append('removing')
825+
logger.removeHandler(self)
826+
class CountingHandler(logging.Handler):
827+
def emit(self, record):
828+
calls.append('counting')
829+
logger.addHandler(RemovingHandler())
830+
logger.addHandler(CountingHandler())
831+
logger.error('spam')
832+
self.assertEqual(calls, ['removing', 'counting'])
833+
817834

818835
class BadStream(object):
819836
def write(self, data):

Lib/test/test_zoneinfo/test_zoneinfo.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,18 @@ def test_unambiguous(self):
316316
self.assertEqual(dt.utcoffset(), offset.utcoffset, dt)
317317
self.assertEqual(dt.dst(), offset.dst, dt)
318318

319+
def test_datetime_subclass_negative_components(self):
320+
class MinusOneDateTime(datetime):
321+
hour = minute = second = -1
322+
323+
zi = self.zone_from_key("UTC")
324+
dt = MinusOneDateTime(2024, 1, 1, tzinfo=zi)
325+
326+
self.assertEqual(dt.utcoffset(), ZERO)
327+
self.assertEqual(dt.dst(), ZERO)
328+
self.assertEqual(dt.tzname(), "UTC")
329+
self.assertEqual(zi.fromutc(dt), datetime(2024, 1, 1, tzinfo=zi))
330+
319331
def test_folds_and_gaps(self):
320332
test_cases = []
321333
for key in self.zones():
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed a race condition in :mod:`logging`:
2+
if a handler was removed while a record was being emitted,
3+
the following handlers of the same logger could be skipped.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a bug in the C accelerator for :mod:`zoneinfo` where
2+
:class:`datetime.datetime` subclasses returning ``-1`` for ``hour``,
3+
``minute``, or ``second`` could incorrectly raise a :exc:`SystemError`.
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`.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`decimal.localcontext` now raises :exc:`TypeError` if a keyword argument
2+
is ``None``, as the pure Python implementation already did. Previously the C
3+
implementation silently ignored it.

0 commit comments

Comments
 (0)