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
2 changes: 1 addition & 1 deletion Doc/reference/compound_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ A literal pattern corresponds to most
: | "None"
: | "True"
: | "False"
signed_number: ["-"] NUMBER
signed_number: ["+" | "-"] NUMBER

The rule ``strings`` and the token ``NUMBER`` are defined in the
:doc:`standard Python grammar <./grammar>`. Triple-quoted strings are
Expand Down
4 changes: 4 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,10 @@ Other language changes
* Allow the *count* argument of :meth:`bytes.replace` to be a keyword.
(Contributed by Stan Ulbrych in :gh:`147856`.)

* Unary plus is now accepted in :keyword:`match` literal patterns, mirroring
the existing support for unary minus.
(Contributed by Bartosz Sławecki in :gh:`145239`.)


New modules
===========
Expand Down
3 changes: 3 additions & 0 deletions Grammar/python.gram
Original file line number Diff line number Diff line change
Expand Up @@ -554,17 +554,20 @@ complex_number[expr_ty]:

signed_number[expr_ty]:
| NUMBER
| '+' number=NUMBER { number }
| '-' number=NUMBER { _PyAST_UnaryOp(USub, number, EXTRA) }

signed_real_number[expr_ty]:
| real_number
| '+' real=real_number { real }
| '-' real=real_number { _PyAST_UnaryOp(USub, real, EXTRA) }

real_number[expr_ty]:
| real=NUMBER { _PyPegen_ensure_real(p, real) }

imaginary_number[expr_ty]:
| imag=NUMBER { _PyPegen_ensure_imaginary(p, imag) }
| '+' imag=NUMBER { _PyPegen_ensure_imaginary(p, imag) }

capture_pattern[pattern_ty]:
| target=pattern_capture_target { _PyAST_MatchAs(NULL, target->v.Name.id, EXTRA) }
Expand Down
2 changes: 2 additions & 0 deletions Lib/test/.ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ extend-exclude = [
"test_lazy_import/__init__.py",
"test_lazy_import/data/*.py",
"test_lazy_import/data/**/*.py",
# Unary plus literal pattern is not yet supported by Ruff (GH-145239)
"test_patma.py",
]

[lint]
Expand Down
22 changes: 21 additions & 1 deletion Lib/test/test_free_threading/test_str.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import sys
import threading
import unittest

from itertools import cycle
from threading import Event, Thread
from threading import Barrier, Event, Thread
from unittest import TestCase

from test.support import threading_helper
Expand Down Expand Up @@ -69,6 +71,24 @@ def reader_func():
for reader in readers:
reader.join()

def test_intern_unowned_string(self):
# Test interning strings owned by various threads.
strings = [f"intern_race_owner_{i}" for i in range(50)]

NUM_THREADS = 5
b = Barrier(NUM_THREADS)

def interner():
tid = threading.get_ident()
for i in range(20):
strings.append(f"intern_{tid}_{i}")
b.wait()
for s in strings:
r = sys.intern(s)
self.assertTrue(sys._is_interned(r))

threading_helper.run_concurrently(interner, nthreads=NUM_THREADS)

def test_maketrans_dict_concurrent_modification(self):
for _ in range(5):
d = {2000: 'a'}
Expand Down
90 changes: 90 additions & 0 deletions Lib/test/test_patma.py
Original file line number Diff line number Diff line change
Expand Up @@ -2762,6 +2762,96 @@ def test_patma_255(self):
self.assertEqual(y, 1)
self.assertIs(z, x)

def test_patma_256(self):
x = 0
match x:
case +0:
y = 0
self.assertEqual(x, 0)
self.assertEqual(y, 0)

def test_patma_257(self):
x = 0
match x:
case +0.0:
y = 0
self.assertEqual(x, 0)
self.assertEqual(y, 0)

def test_patma_258(self):
x = 0
match x:
case +0j:
y = 0
self.assertEqual(x, 0)
self.assertEqual(y, 0)

def test_patma_259(self):
x = 0
match x:
case +0.0j:
y = 0
self.assertEqual(x, 0)
self.assertEqual(y, 0)

def test_patma_260(self):
x = 1
match x:
case +1:
y = 0
self.assertEqual(x, 1)
self.assertEqual(y, 0)

def test_patma_261(self):
x = 1.5
match x:
case +1.5:
y = 0
self.assertEqual(x, 1.5)
self.assertEqual(y, 0)

def test_patma_262(self):
x = 1j
match x:
case +1j:
y = 0
self.assertEqual(x, 1j)
self.assertEqual(y, 0)

def test_patma_263(self):
x = 1.5j
match x:
case +1.5j:
y = 0
self.assertEqual(x, 1.5j)
self.assertEqual(y, 0)

def test_patma_264(self):
x = 0.25 + 1.75j
match x:
case +0.25 + 1.75j:
y = 0
self.assertEqual(x, 0.25 + 1.75j)
self.assertEqual(y, 0)

def test_patma_265(self):
x = 0.25 - 1.75j
match x:
case 0.25 - +1.75j:
y = 0
self.assertEqual(x, 0.25 - 1.75j)
self.assertEqual(y, 0)

def test_patma_266(self):
x = 0
match x:
case +1e1000:
y = 0
case 0:
y = 1
self.assertEqual(x, 0)
self.assertEqual(y, 1)

def test_patma_runtime_checkable_protocol(self):
# Runtime-checkable protocol
from typing import Protocol, runtime_checkable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Unary plus is now accepted in :keyword:`match` literal patterns, mirroring
the existing support for unary minus.
Patch by Bartosz Sławecki.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a data race in :func:`sys.intern` in the free-threaded build when
interning a string owned by another thread. An interned copy owned by the
current thread is used instead when it is not safe to immortalize the
original.
6 changes: 3 additions & 3 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -2768,9 +2768,9 @@ _Py_SetImmortalUntracked(PyObject *op)
return;
}
#ifdef Py_GIL_DISABLED
op->ob_tid = _Py_UNOWNED_TID;
op->ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL;
op->ob_ref_shared = 0;
_Py_atomic_store_uintptr_relaxed(&op->ob_tid, _Py_UNOWNED_TID);
_Py_atomic_store_uint32_relaxed(&op->ob_ref_local, _Py_IMMORTAL_REFCNT_LOCAL);
_Py_atomic_store_ssize_relaxed(&op->ob_ref_shared, 0);
_Py_atomic_or_uint8(&op->ob_gc_bits, _PyGC_BITS_DEFERRED);
#elif SIZEOF_VOID_P > 4
op->ob_flags = _Py_IMMORTAL_FLAGS;
Expand Down
56 changes: 49 additions & 7 deletions Objects/unicodeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,14 @@ _PyUnicode_CheckConsistency(PyObject *op, int check_content)
{
#define CHECK(expr) \
do { if (!(expr)) { _PyObject_ASSERT_FAILED_MSG(op, Py_STRINGIFY(expr)); } } while (0)
#ifdef Py_GIL_DISABLED
# define CHECK_IF_GIL(expr) (void)(expr)
# define CHECK_IF_FT(expr) CHECK(expr)
#else
# define CHECK_IF_GIL(expr) CHECK(expr)
# define CHECK_IF_FT(expr) (void)(expr)
#endif


assert(op != NULL);
CHECK(PyUnicode_Check(op));
Expand Down Expand Up @@ -669,11 +677,9 @@ _PyUnicode_CheckConsistency(PyObject *op, int check_content)

/* Check interning state */
#ifdef Py_DEBUG
// Note that we do not check `_Py_IsImmortal(op)`, since stable ABI
// extensions can make immortal strings mortal (but with a high enough
// refcount).
// The other way is extremely unlikely (worth a potential failed assertion
// in a debug build), so we do check `!_Py_IsImmortal(op)`.
// Note that we do not check `_Py_IsImmortal(op)` in the GIL-enabled build
// since stable ABI extensions can make immortal strings mortal (but with a
// high enough refcount).
switch (PyUnicode_CHECK_INTERNED(op)) {
case SSTATE_NOT_INTERNED:
if (ascii->state.statically_allocated) {
Expand All @@ -683,18 +689,20 @@ _PyUnicode_CheckConsistency(PyObject *op, int check_content)
// are static but use SSTATE_NOT_INTERNED
}
else {
CHECK(!_Py_IsImmortal(op));
CHECK_IF_GIL(!_Py_IsImmortal(op));
}
break;
case SSTATE_INTERNED_MORTAL:
CHECK(!ascii->state.statically_allocated);
CHECK(!_Py_IsImmortal(op));
CHECK_IF_GIL(!_Py_IsImmortal(op));
break;
case SSTATE_INTERNED_IMMORTAL:
CHECK(!ascii->state.statically_allocated);
CHECK_IF_FT(_Py_IsImmortal(op));
break;
case SSTATE_INTERNED_IMMORTAL_STATIC:
CHECK(ascii->state.statically_allocated);
CHECK_IF_FT(_Py_IsImmortal(op));
break;
default:
Py_UNREACHABLE();
Expand Down Expand Up @@ -14208,6 +14216,18 @@ immortalize_interned(PyObject *s)
FT_ATOMIC_STORE_UINT8(_PyUnicode_STATE(s).interned, SSTATE_INTERNED_IMMORTAL);
}

#ifdef Py_GIL_DISABLED
static bool
can_immortalize_safely(PyObject *s)
{
if (_Py_IsOwnedByCurrentThread(s) || _Py_IsImmortal(s)) {
return true;
}
Py_ssize_t shared = _Py_atomic_load_ssize(&s->ob_ref_shared);
return _Py_REF_IS_MERGED(shared);
}
#endif

static /* non-null */ PyObject*
intern_common(PyInterpreterState *interp, PyObject *s /* stolen */,
bool immortalize)
Expand Down Expand Up @@ -14236,11 +14256,16 @@ intern_common(PyInterpreterState *interp, PyObject *s /* stolen */,
// no, go on
break;
case SSTATE_INTERNED_MORTAL:
#ifndef Py_GIL_DISABLED
// yes but we might need to make it immortal
if (immortalize) {
immortalize_interned(s);
}
return s;
#else
// not fully interned yet; fall through to the locking path
break;
#endif
default:
// all done
return s;
Expand Down Expand Up @@ -14305,6 +14330,23 @@ intern_common(PyInterpreterState *interp, PyObject *s /* stolen */,
Py_DECREF(r);
}
#endif

#ifdef Py_GIL_DISABLED
// Immortalization writes to the refcount fields non-atomically. That
// races with Py_INCREF / Py_DECREF on the thread that owns `s`. If we
// don't own it (and its refcount hasn't been merged), intern a copy
// we own instead.
if (!can_immortalize_safely(s)) {
PyObject *copy = _PyUnicode_Copy(s);
if (copy == NULL) {
PyErr_Clear();
return s;
}
Py_DECREF(s);
s = copy;
}
#endif

FT_MUTEX_LOCK(INTERN_MUTEX);
PyObject *t;
{
Expand Down
Loading
Loading