Skip to content

Commit 6729677

Browse files
authored
Merge branch 'main' into main
2 parents 8cc270a + b9d4318 commit 6729677

25 files changed

+412
-168
lines changed

Doc/c-api/list.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,25 @@ List Objects
7474
Like :c:func:`PyList_GetItemRef`, but returns a
7575
:term:`borrowed reference` instead of a :term:`strong reference`.
7676
77+
.. note::
78+
79+
In the :term:`free-threaded build`, the returned
80+
:term:`borrowed reference` may become invalid if another thread modifies
81+
the list concurrently. Prefer :c:func:`PyList_GetItemRef`, which returns
82+
a :term:`strong reference`.
83+
7784
7885
.. c:function:: PyObject* PyList_GET_ITEM(PyObject *list, Py_ssize_t i)
7986
8087
Similar to :c:func:`PyList_GetItem`, but without error checking.
8188
89+
.. note::
90+
91+
In the :term:`free-threaded build`, the returned
92+
:term:`borrowed reference` may become invalid if another thread modifies
93+
the list concurrently. Prefer :c:func:`PyList_GetItemRef`, which returns
94+
a :term:`strong reference`.
95+
8296
8397
.. c:function:: int PyList_SetItem(PyObject *list, Py_ssize_t index, PyObject *item)
8498
@@ -108,6 +122,14 @@ List Objects
108122
is being replaced; any reference in *list* at position *i* will be
109123
leaked.
110124
125+
.. note::
126+
127+
In the :term:`free-threaded build`, this macro has no internal
128+
synchronization. It is normally only used to fill in new lists where no
129+
other thread has a reference to the list. If the list may be shared,
130+
use :c:func:`PyList_SetItem` instead, which uses a :term:`per-object
131+
lock`.
132+
111133
112134
.. c:function:: int PyList_Insert(PyObject *list, Py_ssize_t index, PyObject *item)
113135
@@ -138,6 +160,12 @@ List Objects
138160
Return ``0`` on success, ``-1`` on failure. Indexing from the end of the
139161
list is not supported.
140162
163+
.. note::
164+
165+
In the :term:`free-threaded build`, when *itemlist* is a :class:`list`,
166+
both *list* and *itemlist* are locked for the duration of the operation.
167+
For other iterables (or ``NULL``), only *list* is locked.
168+
141169
142170
.. c:function:: int PyList_Extend(PyObject *list, PyObject *iterable)
143171
@@ -150,6 +178,14 @@ List Objects
150178
151179
.. versionadded:: 3.13
152180
181+
.. note::
182+
183+
In the :term:`free-threaded build`, when *iterable* is a :class:`list`,
184+
:class:`set`, :class:`dict`, or dict view, both *list* and *iterable*
185+
(or its underlying dict) are locked for the duration of the operation.
186+
For other iterables, only *list* is locked; *iterable* may be
187+
concurrently modified by another thread.
188+
153189
154190
.. c:function:: int PyList_Clear(PyObject *list)
155191
@@ -168,6 +204,14 @@ List Objects
168204
Sort the items of *list* in place. Return ``0`` on success, ``-1`` on
169205
failure. This is equivalent to ``list.sort()``.
170206
207+
.. note::
208+
209+
In the :term:`free-threaded build`, element comparison via
210+
:meth:`~object.__lt__` can execute arbitrary Python code, during which
211+
the :term:`per-object lock` may be temporarily released. For built-in
212+
types (:class:`str`, :class:`int`, :class:`float`), the lock is not
213+
released during comparison.
214+
171215
172216
.. c:function:: int PyList_Reverse(PyObject *list)
173217

Doc/data/threadsafety.dat

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,59 @@
1717
PyMutex_Lock:shared:
1818
PyMutex_Unlock:shared:
1919
PyMutex_IsLocked:atomic:
20+
21+
# List objects (Doc/c-api/list.rst)
22+
23+
# Type checks - read ob_type pointer, always safe
24+
PyList_Check:atomic:
25+
PyList_CheckExact:atomic:
26+
27+
# Creation - pure allocation, no shared state
28+
PyList_New:atomic:
29+
30+
# Size - uses atomic load on free-threaded builds
31+
PyList_Size:atomic:
32+
PyList_GET_SIZE:atomic:
33+
34+
# Strong-reference lookup - lock-free with atomic ops
35+
PyList_GetItemRef:atomic:
36+
37+
# Borrowed-reference lookups - no locking; returned borrowed
38+
# reference is unsafe in free-threaded builds without
39+
# external synchronization
40+
PyList_GetItem:compatible:
41+
PyList_GET_ITEM:compatible:
42+
43+
# Single-item mutations - hold per-object lock for duration;
44+
# appear atomic to lock-free readers
45+
PyList_SetItem:atomic:
46+
PyList_Append:atomic:
47+
48+
# Insert - protected by per-object critical section; shifts
49+
# elements so lock-free readers may observe intermediate states
50+
PyList_Insert:shared:
51+
52+
# Initialization macro - no synchronization; normally only used
53+
# to fill in new lists where there is no previous content
54+
PyList_SET_ITEM:compatible:
55+
56+
# Bulk operations - hold per-object lock for duration
57+
PyList_GetSlice:atomic:
58+
PyList_AsTuple:atomic:
59+
PyList_Clear:atomic:
60+
61+
# Reverse - protected by per-object critical section; swaps
62+
# elements so lock-free readers may observe intermediate states
63+
PyList_Reverse:shared:
64+
65+
# Slice assignment - lock target list; also lock source when it
66+
# is a list
67+
PyList_SetSlice:shared:
68+
69+
# Sort - per-object lock held; comparison callbacks may execute
70+
# arbitrary Python code
71+
PyList_Sort:shared:
72+
73+
# Extend - lock target list; also lock source when it is a
74+
# list, set, or dict
75+
PyList_Extend:shared:

Doc/tutorial/errors.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -549,9 +549,9 @@ caught like any other exception. ::
549549
>>> try:
550550
... f()
551551
... except Exception as e:
552-
... print(f'caught {type(e)}: e')
552+
... print(f'caught {type(e)}: {e}')
553553
...
554-
caught <class 'ExceptionGroup'>: e
554+
caught <class 'ExceptionGroup'>: there were problems (2 sub-exceptions)
555555
>>>
556556

557557
By using ``except*`` instead of ``except``, we can selectively

Doc/whatsnew/3.15.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ The following standard library modules have been updated to accept
221221
:func:`eval` and :func:`exec` accept :class:`!frozendict` for *globals*, and
222222
:func:`type` and :meth:`str.maketrans` accept :class:`!frozendict` for *dict*.
223223

224+
Code checking for :class:`dict` type using ``isinstance(arg, dict)`` can be
225+
updated to ``isinstance(arg, (dict, frozendict))`` to accept also the
226+
:class:`!frozendict` type, or to ``isinstance(arg, collections.abc.Mapping)``
227+
to accept also other mapping types such as :class:`~types.MappingProxyType`.
228+
224229
.. seealso:: :pep:`814` for the full specification and rationale.
225230

226231
(Contributed by Victor Stinner and Donghee Na in :gh:`141510`.)

Include/internal/pycore_optimizer.h

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,87 @@ PyAPI_FUNC(_PyExecutorObject*) _Py_GetExecutor(PyCodeObject *code, int offset);
159159

160160
int _Py_ExecutorInit(_PyExecutorObject *, const _PyBloomFilter *);
161161
void _Py_ExecutorDetach(_PyExecutorObject *);
162-
void _Py_BloomFilter_Init(_PyBloomFilter *);
163-
void _Py_BloomFilter_Add(_PyBloomFilter *bloom, void *obj);
164162
PyAPI_FUNC(void) _Py_Executor_DependsOn(_PyExecutorObject *executor, void *obj);
165163

164+
/* We use a bloomfilter with k = 6, m = 256
165+
* The choice of k and the following constants
166+
* could do with a more rigorous analysis,
167+
* but here is a simple analysis:
168+
*
169+
* We want to keep the false positive rate low.
170+
* For n = 5 (a trace depends on 5 objects),
171+
* we expect 30 bits set, giving a false positive
172+
* rate of (30/256)**6 == 2.5e-6 which is plenty
173+
* good enough.
174+
*
175+
* However with n = 10 we expect 60 bits set (worst case),
176+
* giving a false positive of (60/256)**6 == 0.0001
177+
*
178+
* We choose k = 6, rather than a higher number as
179+
* it means the false positive rate grows slower for high n.
180+
*
181+
* n = 5, k = 6 => fp = 2.6e-6
182+
* n = 5, k = 8 => fp = 3.5e-7
183+
* n = 10, k = 6 => fp = 1.6e-4
184+
* n = 10, k = 8 => fp = 0.9e-4
185+
* n = 15, k = 6 => fp = 0.18%
186+
* n = 15, k = 8 => fp = 0.23%
187+
* n = 20, k = 6 => fp = 1.1%
188+
* n = 20, k = 8 => fp = 2.3%
189+
*
190+
* The above analysis assumes perfect hash functions,
191+
* but those don't exist, so the real false positive
192+
* rates may be worse.
193+
*/
194+
195+
#define _Py_BLOOM_FILTER_K 6
196+
#define _Py_BLOOM_FILTER_SEED 20221211
197+
198+
static inline uint64_t
199+
address_to_hash(void *ptr) {
200+
assert(ptr != NULL);
201+
uint64_t uhash = _Py_BLOOM_FILTER_SEED;
202+
uintptr_t addr = (uintptr_t)ptr;
203+
for (int i = 0; i < SIZEOF_VOID_P; i++) {
204+
uhash ^= addr & 255;
205+
uhash *= (uint64_t)PyHASH_MULTIPLIER;
206+
addr >>= 8;
207+
}
208+
return uhash;
209+
}
210+
211+
static inline void
212+
_Py_BloomFilter_Init(_PyBloomFilter *bloom)
213+
{
214+
for (int i = 0; i < _Py_BLOOM_FILTER_WORDS; i++) {
215+
bloom->bits[i] = 0;
216+
}
217+
}
218+
219+
static inline void
220+
_Py_BloomFilter_Add(_PyBloomFilter *bloom, void *ptr)
221+
{
222+
uint64_t hash = address_to_hash(ptr);
223+
assert(_Py_BLOOM_FILTER_K <= 8);
224+
for (int i = 0; i < _Py_BLOOM_FILTER_K; i++) {
225+
uint8_t bits = hash & 255;
226+
bloom->bits[bits >> _Py_BLOOM_FILTER_WORD_SHIFT] |=
227+
(_Py_bloom_filter_word_t)1 << (bits & (_Py_BLOOM_FILTER_BITS_PER_WORD - 1));
228+
hash >>= 8;
229+
}
230+
}
231+
232+
static inline bool
233+
bloom_filter_may_contain(const _PyBloomFilter *bloom, const _PyBloomFilter *hashes)
234+
{
235+
for (int i = 0; i < _Py_BLOOM_FILTER_WORDS; i++) {
236+
if ((bloom->bits[i] & hashes->bits[i]) != hashes->bits[i]) {
237+
return false;
238+
}
239+
}
240+
return true;
241+
}
242+
166243
#define _Py_MAX_ALLOWED_BUILTINS_MODIFICATIONS 3
167244
#define _Py_MAX_ALLOWED_GLOBALS_MODIFICATIONS 6
168245

Include/internal/pycore_uop.h

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,21 @@ typedef struct _PyUOpInstruction{
4545

4646
/* Bloom filter with m = 256
4747
* https://en.wikipedia.org/wiki/Bloom_filter */
48-
#define _Py_BLOOM_FILTER_WORDS 8
48+
#ifdef HAVE_GCC_UINT128_T
49+
#define _Py_BLOOM_FILTER_WORDS 2
50+
typedef __uint128_t _Py_bloom_filter_word_t;
51+
#else
52+
#define _Py_BLOOM_FILTER_WORDS 4
53+
typedef uint64_t _Py_bloom_filter_word_t;
54+
#endif
55+
56+
#define _Py_BLOOM_FILTER_BITS_PER_WORD \
57+
((int)(sizeof(_Py_bloom_filter_word_t) * 8))
58+
#define _Py_BLOOM_FILTER_WORD_SHIFT \
59+
((sizeof(_Py_bloom_filter_word_t) == 16) ? 7 : 6)
4960

5061
typedef struct {
51-
uint32_t bits[_Py_BLOOM_FILTER_WORDS];
62+
_Py_bloom_filter_word_t bits[_Py_BLOOM_FILTER_WORDS];
5263
} _PyBloomFilter;
5364

5465
#ifdef __cplusplus

Lib/argparse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2623,7 +2623,7 @@ def _get_nargs_pattern(self, action):
26232623

26242624
# allow any number of options or arguments
26252625
elif nargs == REMAINDER:
2626-
nargs_pattern = '([AO]*)' if option else '(.*)'
2626+
nargs_pattern = '(.*)'
26272627

26282628
# allow one argument followed by any number of options or arguments
26292629
elif nargs == PARSER:

Lib/pkgutil.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,9 @@ def get_data(package, resource):
393393
# signature - an os.path format "filename" starting with the dirname of
394394
# the package's __file__
395395
parts = resource.split('/')
396+
if os.path.isabs(resource) or '..' in parts:
397+
raise ValueError("resource must be a relative path with no "
398+
"parent directory components")
396399
parts.insert(0, os.path.dirname(mod.__file__))
397400
resource_name = os.path.join(*parts)
398401
return loader.get_data(resource_name)

Lib/sysconfig/__init__.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -665,12 +665,10 @@ def get_platform():
665665
666666
For other non-POSIX platforms, currently just returns :data:`sys.platform`."""
667667
if os.name == 'nt':
668-
if 'amd64' in sys.version.lower():
669-
return 'win-amd64'
670-
if '(arm)' in sys.version.lower():
671-
return 'win-arm32'
672-
if '(arm64)' in sys.version.lower():
673-
return 'win-arm64'
668+
import _sysconfig
669+
platform = _sysconfig.get_platform()
670+
if platform:
671+
return platform
674672
return sys.platform
675673

676674
if os.name != "posix" or not hasattr(os, 'uname'):

Lib/test/test_argparse.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6605,6 +6605,20 @@ def test_remainder(self):
66056605
args = parser.parse_args(['--foo', 'a', '--', 'b', '--', 'c'])
66066606
self.assertEqual(NS(foo='a', bar=['--', 'b', '--', 'c']), args)
66076607

6608+
def test_optional_remainder(self):
6609+
parser = argparse.ArgumentParser(exit_on_error=False)
6610+
parser.add_argument('--foo', nargs='...')
6611+
parser.add_argument('bar', nargs='*')
6612+
6613+
args = parser.parse_args(['--', '--foo', 'a', 'b'])
6614+
self.assertEqual(NS(foo=None, bar=['--foo', 'a', 'b']), args)
6615+
args = parser.parse_args(['--foo', '--', 'a', 'b'])
6616+
self.assertEqual(NS(foo=['--', 'a', 'b'], bar=[]), args)
6617+
args = parser.parse_args(['--foo', 'a', '--', 'b'])
6618+
self.assertEqual(NS(foo=['a', '--', 'b'], bar=[]), args)
6619+
args = parser.parse_args(['--foo', 'a', 'b', '--'])
6620+
self.assertEqual(NS(foo=['a', 'b', '--'], bar=[]), args)
6621+
66086622
def test_subparser(self):
66096623
parser = argparse.ArgumentParser(exit_on_error=False)
66106624
parser.add_argument('foo')

0 commit comments

Comments
 (0)