Skip to content

Commit d330317

Browse files
authored
Merge branch 'main' into deprecate-__getformat__/145633
2 parents 6dec973 + 204feba commit d330317

30 files changed

Lines changed: 407 additions & 69 deletions

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ jobs:
338338
with:
339339
persist-credentials: false
340340
- name: Build and test
341-
run: JAVA_HOME="${JAVA_HOME_21_X64:-$JAVA_HOME_21_arm64}" python3 Platforms/Android ci --fast-ci ${{ matrix.arch }}-linux-android
341+
run: python3 Platforms/Android ci --fast-ci ${{ matrix.arch }}-linux-android
342342

343343
build-ios:
344344
name: iOS

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

Include/internal/pycore_lock.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ _PyMutex_at_fork_reinit(PyMutex *m)
3434

3535
typedef enum _PyLockFlags {
3636
// Do not detach/release the GIL when waiting on the lock.
37+
//
38+
// Note that code executed while holding a mutex with this flag must
39+
// not detach, reach a safepoint or initiate a stop-the-world pause.
40+
// Otherwise, a non-detaching waiter may remain waiting for this mutex and
41+
// prevent the pause from completing.
3742
_Py_LOCK_DONT_DETACH = 0,
3843

3944
// Detach/release the GIL while waiting on the lock.

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_asyncio/test_tasks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2924,6 +2924,16 @@ async def coro():
29242924
with self.assertRaises(AttributeError):
29252925
del task._log_destroy_pending
29262926

2927+
def test_get_context_uninitialized_segfault(self):
2928+
# https://github.com/python/cpython/issues/154871
2929+
2930+
class UninitializedTask(self.Task):
2931+
def __init__(self, *args, **kwargs):
2932+
pass
2933+
2934+
task = UninitializedTask()
2935+
self.assertIsNone(task.get_context())
2936+
29272937

29282938
@unittest.skipUnless(hasattr(futures, '_CFuture') and
29292939
hasattr(tasks, '_CTask'),

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_free_threading/test_collections.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import unittest
2-
from collections import deque
2+
from collections import OrderedDict, deque
33
from copy import copy
44
from test.support import threading_helper
55

@@ -49,5 +49,30 @@ def mutate():
4949
)
5050

5151

52+
class TestOrderedDict(unittest.TestCase):
53+
def test_iterator_update_clear_race(self):
54+
# gh-151627: OrderedDict iterator construction must not race with
55+
# concurrent clear()/update() operations that mutate the linked list.
56+
od = OrderedDict((i, i) for i in range(100))
57+
58+
def mutate():
59+
for i in range(5000):
60+
od.clear()
61+
od.update(((i, i), (i + 1, i + 1), (i + 2, i + 2)))
62+
63+
def iterate():
64+
for _ in range(5000):
65+
try:
66+
for _ in od:
67+
pass
68+
list(reversed(od))
69+
except RuntimeError:
70+
pass
71+
72+
threading_helper.run_concurrently(
73+
[mutate, *[iterate for _ in range(8)]],
74+
)
75+
76+
5277
if __name__ == "__main__":
5378
unittest.main()

0 commit comments

Comments
 (0)