Skip to content

Commit 57174fb

Browse files
committed
Merge branch 'cmaloney/systrace_helper_wip' into cmaloney/pyio_merged
2 parents a6d446e + 3b6c094 commit 57174fb

24 files changed

+460
-103
lines changed

Doc/c-api/long.rst

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -405,14 +405,13 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.
405405
406406
Passing zero to *n_bytes* will return the size of a buffer that would
407407
be large enough to hold the value. This may be larger than technically
408-
necessary, but not unreasonably so.
408+
necessary, but not unreasonably so. If *n_bytes=0*, *buffer* may be
409+
``NULL``.
409410
410411
.. note::
411412
412413
Passing *n_bytes=0* to this function is not an accurate way to determine
413-
the bit length of a value.
414-
415-
If *n_bytes=0*, *buffer* may be ``NULL``.
414+
the bit length of the value.
416415
417416
To get at the entire Python value of an unknown size, the function can be
418417
called twice: first to determine the buffer size, then to fill it::
@@ -462,6 +461,7 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.
462461
.. c:macro:: Py_ASNATIVEBYTES_NATIVE_ENDIAN ``3``
463462
.. c:macro:: Py_ASNATIVEBYTES_UNSIGNED_BUFFER ``4``
464463
.. c:macro:: Py_ASNATIVEBYTES_REJECT_NEGATIVE ``8``
464+
.. c:macro:: Py_ASNATIVEBYTES_ALLOW_INDEX ``16``
465465
============================================= ======
466466
467467
Specifying ``Py_ASNATIVEBYTES_NATIVE_ENDIAN`` will override any other endian
@@ -483,6 +483,13 @@ distinguished from a number. Use :c:func:`PyErr_Occurred` to disambiguate.
483483
provided there is enough space for at least one sign bit, regardless of
484484
whether ``Py_ASNATIVEBYTES_UNSIGNED_BUFFER`` was specified.
485485
486+
If ``Py_ASNATIVEBYTES_ALLOW_INDEX`` is specified and a non-integer value is
487+
passed, its :meth:`~object.__index__` method will be called first. This may
488+
result in Python code executing and other threads being allowed to run, which
489+
could cause changes to other objects or values in use. When *flags* is
490+
``-1``, this option is not set, and non-integer values will raise
491+
:exc:`TypeError`.
492+
486493
.. note::
487494
488495
With the default *flags* (``-1``, or *UNSIGNED_BUFFER* without

Doc/whatsnew/3.14.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,14 @@ symtable
125125
Optimizations
126126
=============
127127

128+
asyncio
129+
-------
130+
131+
* :mod:`asyncio` now uses double linked list implementation for native tasks
132+
which speeds up execution by 10% on standard pyperformance benchmarks and
133+
reduces memory usage.
134+
(Contributed by Kumar Aditya in :gh:`107803`.)
135+
128136

129137

130138

Include/cpython/longobject.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ PyAPI_FUNC(PyObject*) PyLong_FromUnicodeObject(PyObject *u, int base);
1010
#define Py_ASNATIVEBYTES_NATIVE_ENDIAN 3
1111
#define Py_ASNATIVEBYTES_UNSIGNED_BUFFER 4
1212
#define Py_ASNATIVEBYTES_REJECT_NEGATIVE 8
13+
#define Py_ASNATIVEBYTES_ALLOW_INDEX 16
1314

1415
/* PyLong_AsNativeBytes: Copy the integer value to a native variable.
1516
buffer points to the first byte of the variable.
@@ -20,8 +21,10 @@ PyAPI_FUNC(PyObject*) PyLong_FromUnicodeObject(PyObject *u, int base);
2021
* 2 - native endian
2122
* 4 - unsigned destination (e.g. don't reject copying 255 into one byte)
2223
* 8 - raise an exception for negative inputs
23-
If flags is -1 (all bits set), native endian is used and value truncation
24-
behaves most like C (allows negative inputs and allow MSB set).
24+
* 16 - call __index__ on non-int types
25+
If flags is -1 (all bits set), native endian is used, value truncation
26+
behaves most like C (allows negative inputs and allow MSB set), and non-int
27+
objects will raise a TypeError.
2528
Big endian mode will write the most significant byte into the address
2629
directly referenced by buffer; little endian will write the least significant
2730
byte into that address.

Include/pymacro.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
/* Argument must be a char or an int in [-128, 127] or [0, 255]. */
4747
#define Py_CHARMASK(c) ((unsigned char)((c) & 0xff))
4848

49-
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
49+
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L \
50+
&& !defined(_MSC_VER))
5051
# define Py_BUILD_ASSERT_EXPR(cond) \
5152
((void)sizeof(struct { int dummy; _Static_assert(cond, #cond); }), \
5253
0)

Lib/argparse.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1791,7 +1791,7 @@ def _get_kwargs(self):
17911791
# ==================================
17921792
def add_subparsers(self, **kwargs):
17931793
if self._subparsers is not None:
1794-
self.error(_('cannot have multiple subparser arguments'))
1794+
raise ArgumentError(None, _('cannot have multiple subparser arguments'))
17951795

17961796
# add the parser class to the arguments if it's not present
17971797
kwargs.setdefault('parser_class', type(self))
@@ -1846,7 +1846,8 @@ def parse_args(self, args=None, namespace=None):
18461846
msg = _('unrecognized arguments: %s') % ' '.join(argv)
18471847
if self.exit_on_error:
18481848
self.error(msg)
1849-
raise ArgumentError(None, msg)
1849+
else:
1850+
raise ArgumentError(None, msg)
18501851
return args
18511852

18521853
def parse_known_args(self, args=None, namespace=None):
@@ -2135,7 +2136,7 @@ def consume_positionals(start_index):
21352136
self._get_value(action, action.default))
21362137

21372138
if required_actions:
2138-
self.error(_('the following arguments are required: %s') %
2139+
raise ArgumentError(None, _('the following arguments are required: %s') %
21392140
', '.join(required_actions))
21402141

21412142
# make sure all required groups had one option present
@@ -2151,7 +2152,7 @@ def consume_positionals(start_index):
21512152
for action in group._group_actions
21522153
if action.help is not SUPPRESS]
21532154
msg = _('one of the arguments %s is required')
2154-
self.error(msg % ' '.join(names))
2155+
raise ArgumentError(None, msg % ' '.join(names))
21552156

21562157
# return the updated namespace and the extra arguments
21572158
return namespace, extras
@@ -2178,7 +2179,7 @@ def _read_args_from_files(self, arg_strings):
21782179
arg_strings = self._read_args_from_files(arg_strings)
21792180
new_arg_strings.extend(arg_strings)
21802181
except OSError as err:
2181-
self.error(str(err))
2182+
raise ArgumentError(None, str(err))
21822183

21832184
# return the modified argument list
21842185
return new_arg_strings
@@ -2258,7 +2259,7 @@ def _parse_optional(self, arg_string):
22582259
for action, option_string, sep, explicit_arg in option_tuples])
22592260
args = {'option': arg_string, 'matches': options}
22602261
msg = _('ambiguous option: %(option)s could match %(matches)s')
2261-
self.error(msg % args)
2262+
raise ArgumentError(None, msg % args)
22622263

22632264
# if exactly one action matched, this segmentation is good,
22642265
# so return the parsed action
@@ -2318,7 +2319,7 @@ def _get_option_tuples(self, option_string):
23182319

23192320
# shouldn't ever get here
23202321
else:
2321-
self.error(_('unexpected option string: %s') % option_string)
2322+
raise ArgumentError(None, _('unexpected option string: %s') % option_string)
23222323

23232324
# return the collected option tuples
23242325
return result
@@ -2375,8 +2376,11 @@ def _get_nargs_pattern(self, action):
23752376
def parse_intermixed_args(self, args=None, namespace=None):
23762377
args, argv = self.parse_known_intermixed_args(args, namespace)
23772378
if argv:
2378-
msg = _('unrecognized arguments: %s')
2379-
self.error(msg % ' '.join(argv))
2379+
msg = _('unrecognized arguments: %s') % ' '.join(argv)
2380+
if self.exit_on_error:
2381+
self.error(msg)
2382+
else:
2383+
raise ArgumentError(None, msg)
23802384
return args
23812385

23822386
def parse_known_intermixed_args(self, args=None, namespace=None):

Lib/asyncio/unix_events.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -867,9 +867,6 @@ class _PidfdChildWatcher:
867867
recent (5.3+) kernels.
868868
"""
869869

870-
def is_active(self):
871-
return True
872-
873870
def add_child_handler(self, pid, callback, *args):
874871
loop = events.get_running_loop()
875872
pidfd = os.pidfd_open(pid)
@@ -911,9 +908,6 @@ def __init__(self):
911908
self._pid_counter = itertools.count(0)
912909
self._threads = {}
913910

914-
def is_active(self):
915-
return True
916-
917911
def __del__(self, _warn=warnings.warn):
918912
threads = [thread for thread in list(self._threads.values())
919913
if thread.is_alive()]

Lib/test/support/strace_helper.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import re
2+
import sys
3+
import textwrap
4+
import unittest
5+
from dataclasses import dataclass
6+
from test import support
7+
from test.support.script_helper import run_python_until_end
8+
from typing import Dict, List
9+
10+
_strace_binary = "/usr/bin/strace"
11+
_syscall_regex = re.compile(
12+
r"(?P<syscall>[^(]*)\((?P<args>[^)]*)\)\s*[=]\s*(?P<returncode>.+)")
13+
_returncode_regex = re.compile(
14+
r"\+\+\+ exited with (?P<returncode>\d+) \+\+\+")
15+
16+
# Cached value of whether or not there is a compatible strace binary
17+
_strace_working: bool | None = None
18+
19+
20+
@dataclass
21+
class StraceEvent:
22+
syscall: str
23+
args: List[str]
24+
returncode: str
25+
26+
27+
@dataclass
28+
class StraceResult:
29+
strace_returncode: int
30+
python_returncode: int
31+
_raw_events: str
32+
stdout: str
33+
stderr: str
34+
35+
def events(self) -> List[StraceEvent]:
36+
"""Extract the call list information from _raw_events"""
37+
matches = [
38+
_syscall_regex.match(event)
39+
for event in self._raw_events.splitlines()
40+
]
41+
return [
42+
StraceEvent(match["syscall"],
43+
[arg.strip() for arg in (match["args"].split(","))],
44+
match["returncode"]) for match in matches if match
45+
]
46+
47+
def sections(self) -> Dict[str:List[StraceEvent]]:
48+
"""Find all "MARK <X>" writes and use them to make groups of events.
49+
50+
This is useful to avoid variable / overhead strace events, like that
51+
at interpreter startup, so a test can just check does the small case
52+
under study work."""
53+
current_section = "__startup"
54+
sections = {current_section: []}
55+
for event in self.events():
56+
if event.syscall == 'write' and len(
57+
event.args) > 2 and event.args[1].startswith("\"MARK "):
58+
# Found a new section, don't include the write in the section
59+
# but all events until next mark should be in that section
60+
current_section = event.args[1].split(
61+
" ", 1)[1].removesuffix('\\n"')
62+
if current_section not in sections:
63+
sections[current_section] = list()
64+
else:
65+
sections[current_section].append(event)
66+
67+
return sections
68+
69+
70+
@support.requires_subprocess()
71+
def strace_python(code: str,
72+
strace_flags: List[str],
73+
check: bool = True) -> StraceResult:
74+
"""Run strace and return the trace.
75+
76+
Sets strace_returncode and python_returncode to `-1` on error
77+
"""
78+
res = None
79+
80+
def _make_error(reason, details):
81+
return StraceResult(
82+
strace_returncode=-1,
83+
python_returncode=-1,
84+
_raw_events=f"error({reason},details={details}) = -1",
85+
stdout=res.out if res else "",
86+
stderr=res.err if res else "")
87+
88+
# Run strace, and get out the raw text
89+
try:
90+
res, cmd_line = run_python_until_end(
91+
"-c",
92+
textwrap.dedent(code),
93+
__run_using_command=[_strace_binary] + strace_flags)
94+
except OSError as err:
95+
return _make_error("Caught OSError", err)
96+
97+
if check and res.rc:
98+
res.fail(cmd_line)
99+
100+
# Get out program returncode
101+
decoded = res.err.decode().strip()
102+
103+
output = decoded.rsplit("\n", 1)
104+
if len(output) != 2:
105+
return _make_error("Expected strace events and exit code line",
106+
decoded[-50:])
107+
108+
returncode_match = _returncode_regex.match(output[1])
109+
if not returncode_match:
110+
return _make_error("Expected to find returncode in last line.",
111+
output[1][:50])
112+
113+
python_returncode = int(returncode_match["returncode"])
114+
if check and python_returncode:
115+
res.fail(cmd_line)
116+
117+
return StraceResult(strace_returncode=res.rc,
118+
python_returncode=python_returncode,
119+
_raw_events=output[0],
120+
stdout=res.out,
121+
stderr=res.err)
122+
123+
124+
def _get_events(code: str, strace_flags: List[str], prelude: str,
125+
cleanup: str) -> List[StraceEvent]:
126+
# NOTE: The flush is currently required to prevent the prints from getting
127+
# buffered and done all at once at exit
128+
prelude = textwrap.dedent(prelude)
129+
code = textwrap.dedent(code)
130+
cleanup = textwrap.dedent(cleanup)
131+
to_run = f"""
132+
print("MARK prelude", flush=True)
133+
{prelude}
134+
print("MARK code", flush=True)
135+
{code}
136+
print("MARK cleanup", flush=True)
137+
{cleanup}
138+
print("MARK __shutdown", flush=True)
139+
"""
140+
trace = strace_python(to_run, strace_flags)
141+
all_sections = trace.sections()
142+
return all_sections['code']
143+
144+
145+
def get_syscalls(code: str,
146+
strace_flags: List[str],
147+
prelude: str = "",
148+
cleanup: str = "") -> List[str]:
149+
"""Get the syscalls which a given chunk of python code generates"""
150+
events = _get_events(code, strace_flags, prelude=prelude, cleanup=cleanup)
151+
return [ev.syscall for ev in events]
152+
153+
154+
def _can_strace():
155+
res = strace_python("import sys; sys.exit(0)", [], check=False)
156+
assert res.events(), "Should have parsed multiple calls"
157+
158+
global _strace_working
159+
_strace_working = res.strace_returncode == 0 and res.python_returncode == 0
160+
161+
162+
def requires_strace():
163+
if sys.platform != "linux":
164+
return unittest.skip("Linux only, requires strace.")
165+
# Moderately expensive (spawns a subprocess), so share results when possible.
166+
if _strace_working is None:
167+
_can_strace()
168+
169+
assert _strace_working is not None, "Should have been set by _can_strace"
170+
return unittest.skipUnless(_strace_working, "Requires working strace")
171+
172+
173+
__all__ = ["requires_strace", "strace_python", "StraceResult", "StraceEvent"]

0 commit comments

Comments
 (0)