Skip to content

Commit 5ec306e

Browse files
authored
Merge branch 'main' into gh-136053/memory-saferty-TYPE_SLICE
2 parents dbf6ca3 + 5334732 commit 5ec306e

16 files changed

+334
-110
lines changed

Doc/c-api/capsule.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,19 @@ Refer to :ref:`using-capsules` for more information on using these objects.
105105
``module.attribute``. The *name* stored in the capsule must match this
106106
string exactly.
107107
108+
This function splits *name* on the ``.`` character, and imports the first
109+
element. It then processes further elements using attribute lookups.
110+
108111
Return the capsule's internal *pointer* on success. On failure, set an
109112
exception and return ``NULL``.
110113
114+
.. note::
115+
116+
If *name* points to an attribute of some submodule or subpackage, this
117+
submodule or subpackage must be previously imported using other means
118+
(for example, by using :c:func:`PyImport_ImportModule`) for the
119+
attribute lookups to succeed.
120+
111121
.. versionchanged:: 3.3
112122
*no_block* has no effect anymore.
113123

Doc/library/math.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ noted otherwise, all return values are floats.
5959
:func:`isnan(x) <isnan>` Check if *x* is a NaN (not a number)
6060
:func:`ldexp(x, i) <ldexp>` ``x * (2**i)``, inverse of function :func:`frexp`
6161
:func:`nextafter(x, y, steps) <nextafter>` Floating-point value *steps* steps after *x* towards *y*
62+
:func:`signbit(x) <signbit>` Check if *x* is a negative number
6263
:func:`ulp(x) <ulp>` Value of the least significant bit of *x*
6364

6465
**Power, exponential and logarithmic functions**
@@ -431,6 +432,15 @@ Floating point manipulation functions
431432
Added the *steps* argument.
432433

433434

435+
.. function:: signbit(x)
436+
437+
Return ``True`` if the sign of *x* is negative and ``False`` otherwise.
438+
439+
This is useful to detect the sign bit of zeroes, infinities and NaNs.
440+
441+
.. versionadded:: next
442+
443+
434444
.. function:: ulp(x)
435445

436446
Return the value of the least significant bit of the float *x*:

Doc/whatsnew/3.15.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ math
115115
* Add :func:`math.isnormal` and :func:`math.issubnormal` functions.
116116
(Contributed by Sergey B Kirpichev in :gh:`132908`.)
117117

118+
* Add :func:`math.signbit` function.
119+
(Contributed by Bénédikt Tran in :gh:`135853`.)
120+
118121

119122
os.path
120123
-------

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,7 @@ struct _Py_global_strings {
627627
STRUCT_FOR_ID(offset_src)
628628
STRUCT_FOR_ID(on_type_read)
629629
STRUCT_FOR_ID(onceregistry)
630+
STRUCT_FOR_ID(only_active_thread)
630631
STRUCT_FOR_ID(only_keys)
631632
STRUCT_FOR_ID(oparg)
632633
STRUCT_FOR_ID(opcode)

Include/internal/pycore_runtime_init_generated.h

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_external_inspection.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import threading
88
from asyncio import staggered, taskgroups, base_events, tasks
99
from unittest.mock import ANY
10-
from test.support import os_helper, SHORT_TIMEOUT, busy_retry
10+
from test.support import os_helper, SHORT_TIMEOUT, busy_retry, requires_gil_enabled
1111
from test.support.script_helper import make_script
1212
from test.support.socket_helper import find_unused_port
1313

@@ -876,6 +876,126 @@ def test_self_trace(self):
876876
],
877877
)
878878

879+
@skip_if_not_supported
880+
@unittest.skipIf(
881+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
882+
"Test only runs on Linux with process_vm_readv support",
883+
)
884+
@requires_gil_enabled("Free threaded builds don't have an 'active thread'")
885+
def test_only_active_thread(self):
886+
# Test that only_active_thread parameter works correctly
887+
port = find_unused_port()
888+
script = textwrap.dedent(
889+
f"""\
890+
import time, sys, socket, threading
891+
892+
# Connect to the test process
893+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
894+
sock.connect(('localhost', {port}))
895+
896+
def worker_thread(name, barrier, ready_event):
897+
barrier.wait() # Synchronize thread start
898+
ready_event.wait() # Wait for main thread signal
899+
# Sleep to keep thread alive
900+
time.sleep(10_000)
901+
902+
def main_work():
903+
# Do busy work to hold the GIL
904+
sock.sendall(b"working\\n")
905+
count = 0
906+
while count < 100000000:
907+
count += 1
908+
if count % 10000000 == 0:
909+
pass # Keep main thread busy
910+
sock.sendall(b"done\\n")
911+
912+
# Create synchronization primitives
913+
num_threads = 3
914+
barrier = threading.Barrier(num_threads + 1) # +1 for main thread
915+
ready_event = threading.Event()
916+
917+
# Start worker threads
918+
threads = []
919+
for i in range(num_threads):
920+
t = threading.Thread(target=worker_thread, args=(f"Worker-{{i}}", barrier, ready_event))
921+
t.start()
922+
threads.append(t)
923+
924+
# Wait for all threads to be ready
925+
barrier.wait()
926+
927+
# Signal ready to parent process
928+
sock.sendall(b"ready\\n")
929+
930+
# Signal threads to start waiting
931+
ready_event.set()
932+
933+
# Give threads time to start sleeping
934+
time.sleep(0.1)
935+
936+
# Now do busy work to hold the GIL
937+
main_work()
938+
"""
939+
)
940+
941+
with os_helper.temp_dir() as work_dir:
942+
script_dir = os.path.join(work_dir, "script_pkg")
943+
os.mkdir(script_dir)
944+
945+
# Create a socket server to communicate with the target process
946+
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
947+
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
948+
server_socket.bind(("localhost", port))
949+
server_socket.settimeout(SHORT_TIMEOUT)
950+
server_socket.listen(1)
951+
952+
script_name = _make_test_script(script_dir, "script", script)
953+
client_socket = None
954+
try:
955+
p = subprocess.Popen([sys.executable, script_name])
956+
client_socket, _ = server_socket.accept()
957+
server_socket.close()
958+
959+
# Wait for ready signal
960+
response = b""
961+
while b"ready" not in response:
962+
response += client_socket.recv(1024)
963+
964+
# Wait for the main thread to start its busy work
965+
while b"working" not in response:
966+
response += client_socket.recv(1024)
967+
968+
# Get stack trace with all threads
969+
unwinder_all = RemoteUnwinder(p.pid, all_threads=True)
970+
all_traces = unwinder_all.get_stack_trace()
971+
972+
# Get stack trace with only GIL holder
973+
unwinder_gil = RemoteUnwinder(p.pid, only_active_thread=True)
974+
gil_traces = unwinder_gil.get_stack_trace()
975+
976+
except PermissionError:
977+
self.skipTest(
978+
"Insufficient permissions to read the stack trace"
979+
)
980+
finally:
981+
if client_socket is not None:
982+
client_socket.close()
983+
p.kill()
984+
p.terminate()
985+
p.wait(timeout=SHORT_TIMEOUT)
986+
987+
# Verify we got multiple threads in all_traces
988+
self.assertGreater(len(all_traces), 1, "Should have multiple threads")
989+
990+
# Verify we got exactly one thread in gil_traces
991+
self.assertEqual(len(gil_traces), 1, "Should have exactly one GIL holder")
992+
993+
# The GIL holder should be in the all_traces list
994+
gil_thread_id = gil_traces[0][0]
995+
all_thread_ids = [trace[0] for trace in all_traces]
996+
self.assertIn(gil_thread_id, all_thread_ids,
997+
"GIL holder should be among all threads")
998+
879999

8801000
if __name__ == "__main__":
8811001
unittest.main()

Lib/test/test_math.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,19 @@ def testCopysign(self):
475475
# similarly, copysign(2., NAN) could be 2. or -2.
476476
self.assertEqual(abs(math.copysign(2., NAN)), 2.)
477477

478+
def test_signbit(self):
479+
self.assertRaises(TypeError, math.signbit)
480+
self.assertRaises(TypeError, math.signbit, '1.0')
481+
482+
# C11, §7.12.3.6 requires signbit() to return a nonzero value
483+
# if and only if the sign of its argument value is negative,
484+
# but in practice, we are only interested in a boolean value.
485+
self.assertIsInstance(math.signbit(1.0), bool)
486+
487+
for arg in [0., 1., INF, NAN]:
488+
self.assertFalse(math.signbit(arg))
489+
self.assertTrue(math.signbit(-arg))
490+
478491
def testCos(self):
479492
self.assertRaises(TypeError, math.cos)
480493
self.ftest('cos(-pi/2)', math.cos(-math.pi/2), 0, abs_tol=math.ulp(1))
@@ -1387,7 +1400,6 @@ def __rmul__(self, other):
13871400
args = ((-5, -5, 10), (1.5, 4611686018427387904, 2305843009213693952))
13881401
self.assertEqual(sumprod(*args), 0.0)
13891402

1390-
13911403
@requires_IEEE_754
13921404
@unittest.skipIf(HAVE_DOUBLE_ROUNDING,
13931405
"sumprod() accuracy not guaranteed on machines with double rounding")
@@ -2486,7 +2498,6 @@ def test_nextafter(self):
24862498
with self.assertRaises(ValueError):
24872499
math.nextafter(1.0, INF, steps=-1)
24882500

2489-
24902501
@requires_IEEE_754
24912502
def test_ulp(self):
24922503
self.assertEqual(math.ulp(1.0), sys.float_info.epsilon)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`math`: expose C99 :func:`~math.signbit` function to determine whether
2+
the sign bit of a floating-point value is set. Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)