Skip to content

Commit a72e13e

Browse files
generatedunixname1734921407115435meta-codesync[bot]
authored andcommitted
Sync pre-release CPython main branch from GitHub (2026-08-02)
Summary: Imported python/cpython `3.16.0a0` from upstream rev [`204feba`](https://www.github.com/python/cpython/commit/204febac457c39c6622cd6c741d33afdbcca5d55) (committed 2026-08-02 18:10:21+00:00). # Commit Info - Base: (`3.16.0a0`) - [`7b4165b`](https://www.github.com/python/cpython/commit/7b4165b3b07638d8aeab79a880c52f2b51c56f37) (commit date: 2026-08-01 19:54:04+00:00) - Imported: (`3.16.0a0`) - [`204feba`](https://www.github.com/python/cpython/commit/204febac457c39c6622cd6c741d33afdbcca5d55) (commit date: 2026-08-02 18:10:21+00:00) # Noteworthy file changes - Low-signal files (3 added) (NEWS.d, docs, .github) Complete list of added/removed files: https://www.internalfb.com/intern/everpaste/?color=0&handle=GDa8OSaiMlonhhEGAH_Q2WWrPBsDbr0LAAAz Differential Revision: D114556834 fbshipit-source-id: 427dd59a471a012e5d935f2cb5bf0ea31bdd3742
1 parent 1e45fd2 commit a72e13e

13 files changed

Lines changed: 314 additions & 38 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.

Doc/library/functools.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,9 +386,9 @@ The :mod:`!functools` module defines the following functions:
386386
only one positional argument is provided, but there are two placeholders
387387
that must be filled in.
388388

389-
If :func:`!partial` is applied to an existing :func:`!partial` object,
390-
:data:`!Placeholder` sentinels of the input object are filled in with
391-
new positional arguments.
389+
If :func:`!partial` is applied to an existing
390+
:ref:`partial object <partial-objects>`, :data:`!Placeholder` sentinels of the
391+
input object are filled in with new positional arguments.
392392
A placeholder can be retained by inserting a new
393393
:data:`!Placeholder` sentinel to the place held by a previous :data:`!Placeholder`:
394394

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_codecs.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import _codecs
12
import codecs
23
import contextlib
34
import copy
@@ -3735,6 +3736,17 @@ def test_encode_errors(self):
37353736
self.assertEqual(codecs.iconv_encode(enc, 'a€b', 'xmlcharrefreplace')[0],
37363737
b'a&#8364;b')
37373738

3739+
def test_encode_errors_unencodable_replacement(self):
3740+
# Encoding the replacement must not call the error handler again.
3741+
enc = self.require('ASCII')
3742+
codecs.register_error('test.iconv', lambda exc: ('€', exc.end))
3743+
self.addCleanup(_codecs._unregister_error, 'test.iconv')
3744+
with self.assertRaises(UnicodeEncodeError) as cm:
3745+
codecs.iconv_encode(enc, 'a€b', 'test.iconv')
3746+
self.assertEqual((cm.exception.start, cm.exception.end), (1, 2))
3747+
self.assertEqual(cm.exception.reason,
3748+
'unable to encode error handler result')
3749+
37383750
def test_decode_errors(self):
37393751
enc = self.require('ASCII')
37403752
bad = b'a\xffb'

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):
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: 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)