diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 596cb7565a66e7d..c6f0fb8a50917ac 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -843,17 +843,13 @@ Timeouts Wait for the *fut* :ref:`awaitable ` to complete with a timeout. - If *fut* is a coroutine it is automatically scheduled as a Task. - *timeout* can either be ``None`` or a float or int number of seconds to wait for. If *timeout* is ``None``, block until the future completes. - If a timeout occurs, it cancels the task and raises - :exc:`TimeoutError`. + If a timeout occurs, it cancels *fut* and raises :exc:`TimeoutError`. - To avoid the task :meth:`cancellation `, - wrap it in :func:`shield`. + To prevent *fut* from being cancelled, wrap it in :func:`shield`. The function will wait until the future is actually cancelled, so the total wait time may exceed the *timeout*. If an exception @@ -894,6 +890,10 @@ Timeouts .. versionchanged:: 3.11 Raises :exc:`TimeoutError` instead of :exc:`asyncio.TimeoutError`. + .. versionchanged:: 3.12 + Implemented using :func:`asyncio.timeout`, a coroutine passed as *fut* + is no longer wrapped in a :class:`Task` when *timeout* is positive. + Waiting primitives ================== diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index f5c4f0b0c3297ba..954e132617ce42b 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -239,7 +239,17 @@ def connection_made(self, transport): self._over_ssl = transport.get_extra_info('sslcontext') is not None if self._client_connected_cb is not None: writer = StreamWriter(transport, self, reader, self._loop) - res = self._client_connected_cb(reader, writer) + try: + res = self._client_connected_cb(reader, writer) + except Exception as exc: + self._loop.call_exception_handler({ + 'message': 'Unhandled exception in client_connected_cb', + 'exception': exc, + 'transport': transport, + }) + transport.close() + self._strong_reader = None + return if coroutines.iscoroutine(res): def callback(task): if task.cancelled(): diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 7889d4793a5dec3..498eec3f31b292b 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -440,15 +440,13 @@ def _release_waiter(waiter, *args): async def wait_for(fut, timeout): """Wait for the single Future or coroutine to complete, with timeout. - Coroutine will be wrapped in Task. - Returns result of the Future or coroutine. When a timeout occurs, - it cancels the task and raises TimeoutError. To avoid the task - cancellation, wrap it in shield(). + it cancels fut and raises TimeoutError. To prevent fut from being + cancelled, wrap it in shield(). - If the wait is cancelled, the task is also cancelled. + If the wait is cancelled, fut is also cancelled. - If the task suppresses the cancellation and returns a value instead, + If fut suppresses the cancellation and returns a value instead, that value is returned. This function is a coroutine. diff --git a/Lib/concurrent/interpreters/__init__.py b/Lib/concurrent/interpreters/__init__.py index ea4147ee9a25da5..335a744b727d100 100644 --- a/Lib/concurrent/interpreters/__init__.py +++ b/Lib/concurrent/interpreters/__init__.py @@ -68,8 +68,14 @@ def create(): def list_all(): """Return all existing interpreters.""" - return [Interpreter(id, _whence=whence) - for id, whence in _interpreters.list_all(require_ready=True)] + interps = [] + for id, whence in _interpreters.list_all(require_ready=True): + try: + interps.append(Interpreter(id, _whence=whence)) + except InterpreterNotFoundError: + # It was destroyed after it was listed. + pass + return interps def get_current(): diff --git a/Lib/test/_isolated_sample.py b/Lib/test/_isolated_sample.py index 5853b654fc28cb6..3aa58835c883677 100644 --- a/Lib/test/_isolated_sample.py +++ b/Lib/test/_isolated_sample.py @@ -10,6 +10,7 @@ import sys import time import unittest +from test import support from test.support import isolation # DurationSample sleeps this long in the subprocess; a parent-reported duration @@ -178,3 +179,12 @@ class TimeoutSample(unittest.TestCase): @isolation.runInSubprocess(timeout=TIMEOUT) def test_hang(self): time.sleep(TIMEOUT_HANG) + + +class BigmemSample(unittest.TestCase): + + @support.bigmemtest(size=1024, memuse=1) + def test_where_it_runs(self, size): + # A real run is isolated by bigmemtest() itself, a dummy run is not. + self.assertEqual(isolation.runningInSubprocess, + bool(support.real_max_memuse)) diff --git a/Lib/test/clinic.test.c b/Lib/test/clinic.test.c index 3dca8b8d1ed9b99..2ac153ac43e7029 100644 --- a/Lib/test/clinic.test.c +++ b/Lib/test/clinic.test.c @@ -5431,6 +5431,12 @@ Test_property_set(PyObject *self, PyObject *value, void *Py_UNUSED(context)) { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'property' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } return_value = Test_property_set_impl((TestObj *)self, value); return return_value; @@ -5438,7 +5444,40 @@ Test_property_set(PyObject *self, PyObject *value, void *Py_UNUSED(context)) static int Test_property_set_impl(TestObj *self, PyObject *value) -/*[clinic end generated code: output=49f925ab2a33b637 input=3bc3f46a23c83a88]*/ +/*[clinic end generated code: output=ec103a151cf51d25 input=3bc3f46a23c83a88]*/ + +/*[clinic input] +@setter +@deleter +Test.settable_and_deletable +[clinic start generated code]*/ + +#if !defined(Test_settable_and_deletable_DOCSTR) +# define Test_settable_and_deletable_DOCSTR NULL +#endif +#if defined(TEST_SETTABLE_AND_DELETABLE_GETSETDEF) +# undef TEST_SETTABLE_AND_DELETABLE_GETSETDEF +# define TEST_SETTABLE_AND_DELETABLE_GETSETDEF {"settable_and_deletable", (getter)Test_settable_and_deletable_get, (setter)Test_settable_and_deletable_set, Test_settable_and_deletable_DOCSTR}, +#else +# define TEST_SETTABLE_AND_DELETABLE_GETSETDEF {"settable_and_deletable", NULL, (setter)Test_settable_and_deletable_set, NULL}, +#endif + +static int +Test_settable_and_deletable_set_impl(TestObj *self, PyObject *value); + +static int +Test_settable_and_deletable_set(PyObject *self, PyObject *value, void *Py_UNUSED(context)) +{ + int return_value; + + return_value = Test_settable_and_deletable_set_impl((TestObj *)self, value); + + return return_value; +} + +static int +Test_settable_and_deletable_set_impl(TestObj *self, PyObject *value) +/*[clinic end generated code: output=479986d499b2f56d input=f5647f3511b9daea]*/ /*[clinic input] @setter @@ -5463,6 +5502,12 @@ Test_setter_first_with_docstr_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'setter_first_with_docstr' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } return_value = Test_setter_first_with_docstr_set_impl((TestObj *)self, value); return return_value; @@ -5470,7 +5515,7 @@ Test_setter_first_with_docstr_set(PyObject *self, PyObject *value, void *Py_UNUS static int Test_setter_first_with_docstr_set_impl(TestObj *self, PyObject *value) -/*[clinic end generated code: output=5aaf44373c0af545 input=31a045ce11bbe961]*/ +/*[clinic end generated code: output=eac8bafcaa50aa51 input=31a045ce11bbe961]*/ /*[clinic input] @getter diff --git a/Lib/test/memory_watchdog.py b/Lib/test/memory_watchdog.py deleted file mode 100644 index 4a3f66e1f822bab..000000000000000 --- a/Lib/test/memory_watchdog.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Memory watchdog: periodically read the memory usage of the main test process -and print it out, until terminated.""" - - -import sys -import time -from test.libregrtest.utils import get_process_memory_usage - - -ONE_GIB = (1024 ** 3) - - -def watchdog(pid): - while True: - mem = get_process_memory_usage(pid) - if mem is None: - # get_process_memory_usage() is not supported on the platform, - # or something went wrong. Exit since the next call is likely to - # fail the same way. - return - - # Prefer sys.stdout.write() to print() to use a single write() syscall. - # print(msg) calls write(msg.encode()) and then write(b"\n"). - sys.stdout.write(f" ... process data size: {mem / ONE_GIB:.1f} GiB\n") - sys.stdout.flush() - time.sleep(1) - -def main(): - if len(sys.argv) != 2: - print(f"usage: python {sys.argv[0]} pid") - sys.exit(1) - pid = int(sys.argv[1]) - - try: - watchdog(pid) - except KeyboardInterrupt: - pass - -if __name__ == "__main__": - main() diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index da116355eeea154..31e5508dd9b7907 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -1270,26 +1270,17 @@ def set_memlimit(limit: str) -> None: max_memuse = memlimit -class _MemoryWatchdog: - """An object which periodically watches the process' memory consumption - and prints it out. - """ - - def __init__(self): - self.started = False +def _memory_watchdog(pid): + """Return a function printing the memory usage of process *pid*.""" + # Imported here: test.support does not depend on test.libregrtest. + from test.libregrtest.utils import get_process_memory_usage - def start(self): - import subprocess - watchdog_script = findfile("memory_watchdog.py") - cmd = [sys.executable, watchdog_script, str(os.getpid())] - self.mem_watchdog = subprocess.Popen(cmd) - self.started = True - - def stop(self): - if not self.started: - return - self.mem_watchdog.terminate() - self.mem_watchdog.wait() + def watch(): + mem = get_process_memory_usage(pid) + if mem is not None: + print(f" ... process data size: {mem / (1024 ** 3):.1f} GiB", + flush=True) + return watch def bigmemtest(size, memuse, dry_run=True): @@ -1304,8 +1295,14 @@ def bigmemtest(size, memuse, dry_run=True): extra argument. If 'dry_run' is true, the value passed to the test method may be less than the requested value. If 'dry_run' is false, it means the test doesn't support dummy runs when -M is not specified. + + A test that actually allocates the requested memory (that is, one run with + -M) runs in a subprocess, so that the memory it uses and the address space + it fragments are released when it ends. A dummy run stays in the process. """ def decorator(f): + from test.support import isolation + @functools.wraps(f) def wrapper(self): size = wrapper.size @@ -1321,20 +1318,25 @@ def wrapper(self): "not enough memory: %.1fG minimum needed" % (size * memuse / (1024 ** 3))) - if real_max_memuse and verbose: + if (real_max_memuse and verbose + and not isolation.runningInSubprocess): print() peak = (size * memuse) / (1024 ** 3) - print(f" ... expected peak memory use: {peak:.1f} GiB") - watchdog = _MemoryWatchdog() - watchdog.start() - else: - watchdog = None + # Flushed, so that it precedes the memory usage below. + print(f" ... expected peak memory use: {peak:.1f} GiB", + flush=True) + + if (real_max_memuse and has_subprocess_support + and not isolation.runningInSubprocess): + # Watch it from here: the output of the subprocess is captured. + cls = type(self) + qualname = f'{cls.__qualname__}.{f.__name__}' + proc = isolation._start_test(cls.__module__, qualname) + watchdog = _memory_watchdog(proc.pid) if verbose else None + isolation._replay_test(self, *proc.wait(tick=watchdog)) + return - try: - return f(self, maxsize) - finally: - if watchdog: - watchdog.stop() + return f(self, maxsize) wrapper.size = size wrapper.memuse = memuse diff --git a/Lib/test/support/isolation.py b/Lib/test/support/isolation.py index bb4fa6b003cc20c..3cfd406b0f2f0e1 100644 --- a/Lib/test/support/isolation.py +++ b/Lib/test/support/isolation.py @@ -108,12 +108,75 @@ def _child_environ(env): return environ -def _run_in_subprocess(module, qualname, options, env, timeout): - """Run module.qualname (a test method or class) in a fresh subprocess. +class _SubprocessTest: + """A test running in a subprocess, started by _start_test(). - Return ``(payload, output, returncode)``, where *payload* is the decoded - ``{'outcomes': ..., 'durations': ...}`` mapping from the subprocess, or - ``None`` if it did not run to completion (crash, import error, ...). + The parent can watch the subprocess (its pid) while the test runs, and + must wait() for it. + """ + + def __init__(self, proc, result_path): + self._proc = proc + self._result_path = result_path + + @property + def pid(self): + return self._proc.pid + + def wait(self, timeout=None, tick=None, interval=1.0): + """Wait for the test to finish, calling *tick* every *interval* seconds. + + Return ``(payload, output, returncode)``, where *payload* is the + decoded ``{'outcomes': ..., 'durations': ...}`` mapping from the + subprocess, or ``None`` if it did not run to completion (crash, + import error, ...). + """ + import marshal + import subprocess + import time + deadline = None if timeout is None else time.monotonic() + timeout + try: + while True: + step = None if deadline is None else max( + 0.0, deadline - time.monotonic()) + # Wake up for the next tick, unless the timeout comes first. + ticking = tick is not None and (step is None or step > interval) + try: + # communicate(), not wait(): a test writing more than a + # pipe buffer would block. Retrying keeps what it read. + stdout, stderr = self._proc.communicate( + timeout=interval if ticking else step) + break + except subprocess.TimeoutExpired: + if ticking: + tick() + continue + # Report the hang rather than leaving the runner stuck. + self._proc.kill() + stdout, stderr = self._proc.communicate() + raise _SubprocessTestError( + f'test did not complete in a subprocess ' + f'within {timeout} seconds' + ) from _remote(_decode(stdout) + _decode(stderr)) + try: + with open(self._result_path, 'rb') as f: + payload = marshal.load(f) + except (OSError, EOFError, ValueError): + payload = None + output = _decode(stdout) + _decode(stderr) + return payload, output, self._proc.returncode + finally: + try: + os.unlink(self._result_path) + except OSError: + pass + + +def _start_test(module, qualname, options=(), env=None): + """Start module.qualname (a test method or class) in a fresh subprocess. + + Return a _SubprocessTest. Its wait() is what removes the temporary file + the subprocess writes its result to. """ import marshal import subprocess @@ -129,26 +192,16 @@ def _run_in_subprocess(module, qualname, options, env, timeout): cmd = [sys.executable, *options, '-m', 'test.support.subprocess_runner', module, qualname, result_path, marshal.dumps(_child_config()).hex()] - try: - proc = subprocess.run(cmd, capture_output=True, - env=_child_environ(env), timeout=timeout) - except subprocess.TimeoutExpired as exc: - # Report the hang rather than leaving the test runner stuck. - output = _decode(exc.stdout) + _decode(exc.stderr) - raise _SubprocessTestError( - f'test did not complete in a subprocess ' - f'within {timeout} seconds') from _remote(output) - try: - with open(result_path, 'rb') as f: - payload = marshal.load(f) - except (OSError, EOFError, ValueError): - payload = None - finally: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=_child_environ(env)) + except BaseException: try: os.unlink(result_path) except OSError: pass - return payload, _decode(proc.stdout) + _decode(proc.stderr), proc.returncode + raise + return _SubprocessTest(proc, result_path) + def _replay_outcome(test, outcome): @@ -200,6 +253,19 @@ def _check_returncode(returncode, output, what): raise exc from _remote(output) +def _replay_test(test, payload, output, returncode): + """Reproduce in *test* the result that _SubprocessTest.wait() returned.""" + if payload is None: + exc = _SubprocessTestError( + f'test did not complete in a subprocess (exit code {returncode})') + raise exc from _remote(output) + # The parent measures the test method's own duration (the real cost of the + # isolated run, subprocess startup included), so nothing to forward here. + # Replay the outcomes first: a failure of the test itself is more useful. + _replay_outcomes(test, payload['outcomes']) + _check_returncode(returncode, output, 'test') + + def _isolate_method(func, options, env, timeout): @functools.wraps(func) def wrapper(self, /, *args, **kwargs): @@ -209,18 +275,8 @@ def wrapper(self, /, *args, **kwargs): _check_subprocess_support() cls = type(self) qualname = f'{cls.__qualname__}.{func.__name__}' - payload, output, returncode = _run_in_subprocess(cls.__module__, - qualname, options, - env, timeout) - if payload is None: - exc = _SubprocessTestError( - f'test did not complete in a subprocess (exit code {returncode})') - raise exc from _remote(output) - # The parent measures this method's own duration (the real cost of the - # isolated run, subprocess startup included), so nothing to forward here. - # Replay the outcomes first: a failure of the test itself is more useful. - _replay_outcomes(self, payload['outcomes']) - _check_returncode(returncode, output, 'test') + proc = _start_test(cls.__module__, qualname, options, env) + _replay_test(self, *proc.wait(timeout)) return wrapper @@ -244,9 +300,8 @@ def setUpClass(cls): _check_subprocess_support() # Run the whole class in a single subprocess and stash the outcomes # for the test methods to replay. - payload, output, returncode = _run_in_subprocess(cls.__module__, - cls.__qualname__, - options, env, timeout) + proc = _start_test(cls.__module__, cls.__qualname__, options, env) + payload, output, returncode = proc.wait(timeout) if payload is None: exc = _SubprocessTestError( f'class did not complete in a subprocess (exit code {returncode})') diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index 911087a128f9713..172f183849c3057 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -1267,6 +1267,38 @@ async def handle_echo(reader, writer): messages = self._basetest_unhandled_exceptions(handle_echo) self.assertEqual(messages, []) + def test_unhandled_exception_sync_callback(self): + # An exception raised by a plain-function client_connected_cb is + # reported like the coroutine case and the transport is closed. + port = socket_helper.find_unused_port() + + messages = [] + self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx)) + + async def client(): + rd, wr = await asyncio.open_connection('localhost', port) + async with asyncio.timeout(60): + data = await rd.read() + self.assertEqual(data, b'') # the server closed the connection + wr.close() + await wr.wait_closed() + + async def main(): + def handle_echo(reader, writer): + raise Exception('test') + + server = await asyncio.start_server( + handle_echo, 'localhost', port) + await server.start_serving() + await client() + server.close() + await server.wait_closed() + + self.loop.run_until_complete(main()) + + self.assertEqual(messages[0]['message'], + 'Unhandled exception in client_connected_cb') + def test_open_connection_happy_eyeball_refcycles(self): port = socket_helper.find_unused_port() async def main(): diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py index 720b38cb508cbe6..1b9918c6c8f473c 100644 --- a/Lib/test/test_bytes.py +++ b/Lib/test/test_bytes.py @@ -1828,6 +1828,30 @@ def test_setslice_trap(self): b[8:] = b self.assertEqual(b, bytearray(list(range(8)) + list(range(256)))) + def test_setslice_reentrant_resize(self): + # gh-153578: a buffer argument whose __buffer__ resizes the bytearray + # while the buffer is being acquired must not leave the slice bounds + # with lo > hi, which drove a negative-size memmove (an out-of-bounds + # write) in the setslice path reached through extend(). + class Evil: + def __init__(self, resize): + self.resize = resize + def __buffer__(self, flags): + self.resize() + return memoryview(b'ABCDEFGH') + # clear() during __buffer__: extend appends to the emptied bytearray. + b = bytearray(b'x' * 100) + b.extend(Evil(b.clear)) + self.assertEqual(b, b'ABCDEFGH') + # partial shrink during __buffer__. + b = bytearray(b'x' * 100) + b.extend(Evil(lambda: b.__delitem__(slice(30, None)))) + self.assertEqual(b, b'x' * 30 + b'ABCDEFGH') + # grow during __buffer__: the data lands at the original end. + b = bytearray(b'x' * 10) + b.extend(Evil(lambda: b.extend(b'y' * 100))) + self.assertEqual(b, b'x' * 10 + b'ABCDEFGH' + b'y' * 100) + def test_iconcat(self): b = bytearray(b"abc") b1 = b diff --git a/Lib/test/test_clinic.py b/Lib/test/test_clinic.py index 1dc1c4eaaaba196..94a69b6d7309df8 100644 --- a/Lib/test/test_clinic.py +++ b/Lib/test/test_clinic.py @@ -794,6 +794,102 @@ def test_ignore_preprocessor_in_comments(self): """) self.clinic.parse(raw) + def test_getset_in_ifdef(self): + block = """ + /*[clinic input] + output everything block + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + #ifdef CONDITION + /*[clinic input] + @getter + Foo.property + [clinic start generated code]*/ + /*[clinic input] + @setter + Foo.property + [clinic start generated code]*/ + #endif + """ + generated = self.clinic.parse(dedent(block)) + self.assertIn("#if defined(CONDITION)", generated) + # The getset is undefined if the condition is false. + self.assertIn("#ifndef FOO_PROPERTY_GETSETDEF\n" + " #define FOO_PROPERTY_GETSETDEF\n" + "#endif /* !defined(FOO_PROPERTY_GETSETDEF) */", + generated) + + def test_getset_duplicate(self): + for annotation in "@getter", "@setter": + with self.subTest(annotation=annotation): + self.clinic = _make_clinic(filename="test.c") + block = f""" + /*[clinic input] + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + {annotation} + Foo.property + [clinic start generated code]*/ + /*[clinic input] + {annotation} + Foo.property + [clinic start generated code]*/ + """ + kind = 'setter' if annotation == '@setter' else 'getter' + err = f"Cannot apply @{kind} to 'Foo.property' twice" + self.expect_failure(block, err, lineno=10) + + def test_getset_different_c_basename(self): + block = """ + /*[clinic input] + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + @getter + Foo.property as foo_get + [clinic start generated code]*/ + /*[clinic input] + @setter + Foo.property as foo_set + [clinic start generated code]*/ + """ + err = "The accessors of 'Foo.property' must have the same C basename" + self.expect_failure(block, err, lineno=10) + + def test_setter_deletion_check(self): + block = """ + /*[clinic input] + output everything block + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + @setter + Foo.property + [clinic start generated code]*/ + """ + generated = self.clinic.parse(dedent(block)) + self.assertIn("if (value == NULL) {", generated) + self.assertIn("\"attribute 'property' of '%.100s' objects " + "cannot be deleted\"", generated) + + def test_deleter(self): + # @deleter means that the setter is called with NULL to delete + # the attribute, so it checks the value itself. + block = """ + /*[clinic input] + output everything block + class Foo "FooObject *" "&Foo_Type" + [clinic start generated code]*/ + /*[clinic input] + @setter + @deleter + Foo.property + [clinic start generated code]*/ + """ + generated = self.clinic.parse(dedent(block)) + self.assertNotIn("if (value == NULL) {", generated) + def test_var_keyword_non_dict(self): err = "'var_keyword_object' is not a valid converter" block = """ @@ -2671,7 +2767,7 @@ class Foo "" "" {annotation} Foo.property -> int """ - expected_error = f"{annotation} method cannot define a return type" + expected_error = "@getter and @setter methods cannot define a return type" self.expect_failure(block, expected_error, lineno=3) block = f""" @@ -2682,7 +2778,7 @@ class Foo "" "" obj: int / """ - expected_error = f"{annotation} methods cannot define parameters" + expected_error = "@getter and @setter methods cannot define parameters" self.expect_failure(block, expected_error) def test_setter_docstring(self): @@ -2725,9 +2821,51 @@ class Foo "" "" {dup[1]} Foo.property -> int """ - expected_error = "Cannot apply both @getter and @setter to the same function!" + expected_error = (f"Can't set {dup[1]}, " + f"function is not a normal callable") self.expect_failure(block, expected_error, lineno=3) + def test_deleter_without_setter(self): + block = """ + module foo + class Foo "" "" + @deleter + Foo.property + """ + expected_error = "Can't set @deleter, @setter is not applied" + self.expect_failure(block, expected_error, lineno=2) + + block = """ + module foo + class Foo "" "" + @deleter + @setter + Foo.property + """ + self.expect_failure(block, expected_error, lineno=2) + + def test_deleter_twice(self): + block = """ + module foo + class Foo "" "" + @setter + @deleter + @deleter + Foo.property + """ + expected_error = "Cannot apply @deleter twice to the same function!" + self.expect_failure(block, expected_error, lineno=4) + + def test_setter_and_deleter(self): + function = self.parse_function(""" + module foo + class Foo "" "" + @setter + @deleter + Foo.property + """, signatures_in_block=3, function_index=2) + self.assertEqual(function.kind, FunctionKind.SETTER_AND_DELETER) + def test_getset_no_class(self): for annotation in "@getter", "@setter": with self.subTest(annotation=annotation): @@ -3076,6 +3214,64 @@ def test_no_change(self): # if the content does not change self.assertEqual(pre_mtime, post_mtime) + TOUCH_CODE = dedent(""" + /*[clinic input] + module m + [clinic start generated code]*/ + + /*[clinic input] + output everything file + m.func + a: int + / + + Docstring. + [clinic start generated code]*/ + """) + + def test_touch_source(self): + # gh-64595: The build system does not know that the source file + # depends on the file generated from it, so the modification + # times are updated to force the recompilation. + def mtimes(): + return os.stat(fn).st_mtime_ns, os.stat(dest).st_mtime_ns + + def set_mtimes(source, generated): + os.utime(fn, ns=(source, source)) + os.utime(dest, ns=(generated, generated)) + + with os_helper.temp_dir() as tmp_dir: + fn = os.path.join(tmp_dir, "test.c") + with open(fn, "w", encoding="utf-8") as f: + f.write(self.TOUCH_CODE) + dest = self.dest_file(fn) + self.expect_success(fn) + source_mtime, generated_mtime = mtimes() + self.assertGreaterEqual(generated_mtime, source_mtime) + + # The generated file is changed, so both files are touched. + os.unlink(dest) + old = source_mtime - 10**10 + os.utime(fn, ns=(old, old)) + self.expect_success(fn) + source_mtime, generated_mtime = mtimes() + self.assertGreater(source_mtime, old) + self.assertGreaterEqual(generated_mtime, source_mtime) + + # Nothing is changed, but the source file is newer, so only + # the generated file is touched. + set_mtimes(source_mtime - 10**10, source_mtime - 2 * 10**10) + old_source_mtime = os.stat(fn).st_mtime_ns + self.expect_success(fn) + source_mtime, generated_mtime = mtimes() + self.assertEqual(source_mtime, old_source_mtime) + self.assertGreaterEqual(generated_mtime, source_mtime) + + # Nothing is changed and the generated file is newer, + # so no file is touched. + self.expect_success(fn) + self.assertEqual(mtimes(), (source_mtime, generated_mtime)) + def test_cli_force(self): invalid_input = dedent(""" /*[clinic input] diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py index 13d23af5aceb475..aac3cdd717668ca 100644 --- a/Lib/test/test_interpreters/test_api.py +++ b/Lib/test/test_interpreters/test_api.py @@ -289,6 +289,21 @@ def test_idempotent(self): for interp1, interp2 in zip(actual, expected): self.assertIs(interp1, interp2) + def test_destroyed_by_gc(self): + # gh-155997: the interpreter is destroyed while list_all() runs. + interp = interpreters.create() + interpid = interp.id + cycle = [] + cycle.append(cycle) + cycle.append(interp) + # The cycle holds the only reference, so only the collector frees it. + with support.disable_gc(): + del interp, cycle + + with support.gc_threshold(1): + ids = [i.id for i in interpreters.list_all()] + self.assertNotIn(interpid, ids) + def test_created_with_capi(self): mainid, *_ = _interpreters.get_main() interpid1 = _interpreters.create() diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 84b91bb00cdbe4d..243da190e48f5d2 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -1231,17 +1231,28 @@ def test_timeout_reported_as_error(self): self.assertEqual(len(result.errors), 1) self.assertIn(f'within {TIMEOUT} seconds', result.errors[0][1]) + @support.requires_subprocess() + def test_bigmemtest_isolates_a_real_run(self): + # A dummy run (no -M) stays in this process, a real run does not. + for memlimit in (0, support._1G): + with self.subTest(real_max_memuse=memlimit): + with support.swap_attr(support, 'real_max_memuse', memlimit): + result = self._run('BigmemSample') + self.assertEqual(result.testsRun, 1) + self.assertEqual(self._names(result.failures), []) + self.assertEqual(self._names(result.errors), []) + def test_skipped_without_subprocess_support(self): # On a platform without subprocess support the test is skipped in the # parent, before any subprocess is spawned. calls = [] - orig = isolation._run_in_subprocess + orig = isolation._start_test with support.swap_attr(support, 'has_subprocess_support', False): - isolation._run_in_subprocess = lambda *a, **k: calls.append(a) + isolation._start_test = lambda *a, **k: calls.append(a) try: result = self._run('MethodSample.test_pass') finally: - isolation._run_in_subprocess = orig + isolation._start_test = orig self.assertEqual(result.testsRun, 1) self.assertEqual(len(result.skipped), 1) self.assertEqual(calls, []) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-45-00.gh-issue-153578.Qm4Zt9.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-45-00.gh-issue-153578.Qm4Zt9.rst new file mode 100644 index 000000000000000..2d5d4058a04ef2c --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-11-15-45-00.gh-issue-153578.Qm4Zt9.rst @@ -0,0 +1,3 @@ +Fix an out-of-bounds write in :meth:`bytearray.extend` when the bytearray is +resized while the argument's :meth:`~object.__buffer__` is being acquired, for +example by another thread. Patch by tonghuaroot. diff --git a/Misc/NEWS.d/next/Library/2026-08-14-12-46-07.gh-issue-113318.DYGQjo.rst b/Misc/NEWS.d/next/Library/2026-08-14-12-46-07.gh-issue-113318.DYGQjo.rst new file mode 100644 index 000000000000000..4cd4acd01886368 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-14-12-46-07.gh-issue-113318.DYGQjo.rst @@ -0,0 +1,5 @@ +Fix crashes when deleting an attribute whose setter is generated by Argument +Clinic and is not prepared for deletion, among them +:attr:`frame.f_trace_opcodes` and the ``context``, ``owner`` and ``session`` +attributes of ``_ssl._SSLSocket``. +Deleting such attribute now raises :exc:`AttributeError`. diff --git a/Misc/NEWS.d/next/Library/2026-08-17-21-00-00.gh-issue-155941.strmCb.rst b/Misc/NEWS.d/next/Library/2026-08-17-21-00-00.gh-issue-155941.strmCb.rst new file mode 100644 index 000000000000000..6cee7eb4eb9090e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-17-21-00-00.gh-issue-155941.strmCb.rst @@ -0,0 +1,4 @@ +Fix :func:`asyncio.start_server` when a plain-function *client_connected_cb* +raises: the error is now reported like the coroutine case and the transport +is closed, instead of leaving the connection open forever (which also made +:meth:`asyncio.Server.wait_closed` hang). diff --git a/Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst b/Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst new file mode 100644 index 000000000000000..2727ba34aad8ff5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst @@ -0,0 +1,3 @@ +Fix :func:`concurrent.interpreters.list_all`. It failed if an interpreter +was destroyed during the call, in particular by a garbage collection which +finalized the object owning the last reference to it. diff --git a/Misc/NEWS.d/next/Tests/2026-08-18-10-16-44.gh-issue-75876.PEZplM.rst b/Misc/NEWS.d/next/Tests/2026-08-18-10-16-44.gh-issue-75876.PEZplM.rst new file mode 100644 index 000000000000000..ee3da3730932f6e --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-08-18-10-16-44.gh-issue-75876.PEZplM.rst @@ -0,0 +1,6 @@ +A test decorated with :func:`~test.support.bigmemtest` now runs in a +subprocess if it really allocates the memory it asks for (that is, if the +``-M`` option is used), so that the memory it uses and the address space it +fragments are released when it ends. A dummy run stays in the process. The +separate memory watchdog process is no longer needed: the parent process +watches the memory usage of the subprocess. diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst new file mode 100644 index 000000000000000..6c9393347d31ce5 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-06-09-43-16.gh-issue-64595.W75XZK.rst @@ -0,0 +1,5 @@ +Argument Clinic now touches the source file if a file generated from it was +changed, and keeps the generated files newer than the source file. +The build system does not know that the source file depends on the files +generated from it, so it did not recompile the source file if only the +generated file was changed. diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-14-12-45-57.gh-issue-113318.G1B0oH.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-14-12-45-57.gh-issue-113318.G1B0oH.rst new file mode 100644 index 000000000000000..3ea0a37288fe880 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-14-12-45-57.gh-issue-113318.G1B0oH.rst @@ -0,0 +1,6 @@ +Fix Argument Clinic for ``@getter`` and ``@setter`` in a preprocessor +conditional block. +It failed with an internal error. +Argument Clinic now also rejects the accessors of the same attribute with +different C basenames, and the same accessor defined twice, which silently +generated invalid or duplicated entries of :c:type:`PyGetSetDef`. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 41384b388142ccc..a380f8ac72b32f4 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1384,10 +1384,6 @@ _asyncio_Future__asyncio_future_blocking_set_impl(FutureObj *self, if (future_ensure_alive(self)) { return -1; } - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } int is_true = PyObject_IsTrue(value); if (is_true < 0) { @@ -1427,10 +1423,6 @@ static int _asyncio_Future__log_traceback_set_impl(FutureObj *self, PyObject *value) /*[clinic end generated code: output=9ce8e19504f42f54 input=30ac8217754b08c2]*/ { - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } int is_true = PyObject_IsTrue(value); if (is_true < 0) { return -1; @@ -1592,10 +1584,6 @@ static int _asyncio_Future__cancel_message_set_impl(FutureObj *self, PyObject *value) /*[clinic end generated code: output=0854b2f77bff2209 input=f461d17f2d891fad]*/ { - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } Py_INCREF(value); Py_XSETREF(self->fut_cancel_msg, value); return 0; @@ -2450,10 +2438,6 @@ static int _asyncio_Task__log_destroy_pending_set_impl(TaskObj *self, PyObject *value) /*[clinic end generated code: output=7ebc030bb92ec5ce input=49b759c97d1216a4]*/ { - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } int is_true = PyObject_IsTrue(value); if (is_true < 0) { return -1; diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index adfdf44e53604e3..034f26807f84aa8 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -598,13 +598,14 @@ _ctypes_CType_Type___pointer_type___get_impl(PyObject *self) /*[clinic input] @setter +@deleter _ctypes.CType_Type.__pointer_type__ [clinic start generated code]*/ static int _ctypes_CType_Type___pointer_type___set_impl(PyObject *self, PyObject *value) -/*[clinic end generated code: output=6259be8ea21693fa input=a05055fc7f4714b6]*/ +/*[clinic end generated code: output=6259be8ea21693fa input=7e24bceb1676349b]*/ { ctypes_state *st = get_module_state_by_def(Py_TYPE(self)); StgInfo *info; @@ -1491,10 +1492,6 @@ _ctypes_PyCArrayType_Type_raw_set_impl(CDataObject *self, PyObject *value) Py_ssize_t size; Py_buffer view; - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } if (PyObject_GetBuffer(value, &view, PyBUF_SIMPLE) < 0) return -1; size = view.len; @@ -1550,12 +1547,13 @@ _ctypes_PyCArrayType_Type_value_get_impl(CDataObject *self) /*[clinic input] @critical_section @setter +@deleter _ctypes.PyCArrayType_Type.value [clinic start generated code]*/ static int _ctypes_PyCArrayType_Type_value_set_impl(CDataObject *self, PyObject *value) -/*[clinic end generated code: output=39ad655636a28dd5 input=e2e6385fc6ab1a29]*/ +/*[clinic end generated code: output=39ad655636a28dd5 input=167f0935cbb8d489]*/ { const char *ptr; Py_ssize_t size; @@ -3664,12 +3662,13 @@ _validate_paramflags(ctypes_state *st, PyTypeObject *type, PyObject *paramflags, /*[clinic input] @critical_section @setter +@deleter _ctypes.CFuncPtr.errcheck [clinic start generated code]*/ static int _ctypes_CFuncPtr_errcheck_set_impl(PyCFuncPtrObject *self, PyObject *value) -/*[clinic end generated code: output=6580cf1ffdf3b9fb input=84930bb16c490b33]*/ +/*[clinic end generated code: output=6580cf1ffdf3b9fb input=bcd5d3ed1a0c36e9]*/ { if (value && !PyCallable_Check(value)) { PyErr_SetString(PyExc_TypeError, @@ -3701,13 +3700,14 @@ _ctypes_CFuncPtr_errcheck_get_impl(PyCFuncPtrObject *self) /*[clinic input] @setter +@deleter @critical_section _ctypes.CFuncPtr.restype [clinic start generated code]*/ static int _ctypes_CFuncPtr_restype_set_impl(PyCFuncPtrObject *self, PyObject *value) -/*[clinic end generated code: output=0be0a086abbabf18 input=683c3bef4562ccc6]*/ +/*[clinic end generated code: output=0be0a086abbabf18 input=ffc941a26dbb31f3]*/ { PyObject *checker; if (value == NULL) { @@ -3764,13 +3764,14 @@ _ctypes_CFuncPtr_restype_get_impl(PyCFuncPtrObject *self) /*[clinic input] @setter +@deleter @critical_section _ctypes.CFuncPtr.argtypes [clinic start generated code]*/ static int _ctypes_CFuncPtr_argtypes_set_impl(PyCFuncPtrObject *self, PyObject *value) -/*[clinic end generated code: output=596a36e2ae89d7d1 input=c4627573e980aa8b]*/ +/*[clinic end generated code: output=596a36e2ae89d7d1 input=fd012f1fd7cc35be]*/ { if (value == NULL || value == Py_None) { atomic_xsetref(&self->argtypes, NULL); @@ -5413,12 +5414,13 @@ class _ctypes.Simple "CDataObject *" "clinic_state()->Simple_Type" /*[clinic input] @critical_section @setter +@deleter _ctypes.Simple.value [clinic start generated code]*/ static int _ctypes_Simple_value_set_impl(CDataObject *self, PyObject *value) -/*[clinic end generated code: output=f267186118939863 input=977af9dc9e71e857]*/ +/*[clinic end generated code: output=f267186118939863 input=4e6c1143d17c2c3f]*/ { PyObject *result; diff --git a/Modules/_ctypes/clinic/_ctypes.c.h b/Modules/_ctypes/clinic/_ctypes.c.h index 529872f0f17ebe9..221b110cd15d2de 100644 --- a/Modules/_ctypes/clinic/_ctypes.c.h +++ b/Modules/_ctypes/clinic/_ctypes.c.h @@ -477,6 +477,12 @@ _ctypes_PyCArrayType_Type_raw_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'raw' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ctypes_PyCArrayType_Type_raw_set_impl((CDataObject *)self, value); Py_END_CRITICAL_SECTION(); @@ -1052,4 +1058,4 @@ Simple_from_outparm(PyObject *self, PyTypeObject *cls, PyObject *const *args, Py } return Simple_from_outparm_impl(self, cls); } -/*[clinic end generated code: output=22105663d71237ca input=a9049054013a1b77]*/ +/*[clinic end generated code: output=b89feb50c654de3f input=a9049054013a1b77]*/ diff --git a/Modules/_io/clinic/textio.c.h b/Modules/_io/clinic/textio.c.h index 3c682cb2f271aef..10d0f1390cccbb4 100644 --- a/Modules/_io/clinic/textio.c.h +++ b/Modules/_io/clinic/textio.c.h @@ -1325,6 +1325,12 @@ _io_TextIOWrapper__CHUNK_SIZE_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_CHUNK_SIZE' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _io_TextIOWrapper__CHUNK_SIZE_set_impl((textio *)self, value); Py_END_CRITICAL_SECTION(); @@ -1356,4 +1362,4 @@ _io_TextIOWrapper_buffer_get(PyObject *self, void *Py_UNUSED(context)) return return_value; } -/*[clinic end generated code: output=e34c75e1d2a12084 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e93032a0691ff0e4 input=a9049054013a1b77]*/ diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c index 5b3d379e4e75543..5630f0309d98cff 100644 --- a/Modules/_io/textio.c +++ b/Modules/_io/textio.c @@ -3414,10 +3414,6 @@ _io_TextIOWrapper__CHUNK_SIZE_set_impl(textio *self, PyObject *value) { Py_ssize_t n; CHECK_ATTACHED_INT(self); - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } n = PyNumber_AsSsize_t(value, PyExc_ValueError); if (n == -1 && PyErr_Occurred()) return -1; diff --git a/Modules/_sqlite/clinic/cursor.c.h b/Modules/_sqlite/clinic/cursor.c.h index 3cad9f3aef5ecd5..689466b1c2b85a1 100644 --- a/Modules/_sqlite/clinic/cursor.c.h +++ b/Modules/_sqlite/clinic/cursor.c.h @@ -367,8 +367,14 @@ _sqlite3_Cursor_arraysize_set(PyObject *self, PyObject *value, void *Py_UNUSED(c { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'arraysize' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } return_value = _sqlite3_Cursor_arraysize_set_impl((pysqlite_Cursor *)self, value); return return_value; } -/*[clinic end generated code: output=a0e3ebba9e4d0ece input=a9049054013a1b77]*/ +/*[clinic end generated code: output=e7b20358f8213fd7 input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_asynciomodule.c.h b/Modules/clinic/_asynciomodule.c.h index 8bbef6b50231250..14cf5eebc5eec7a 100644 --- a/Modules/clinic/_asynciomodule.c.h +++ b/Modules/clinic/_asynciomodule.c.h @@ -585,6 +585,12 @@ _asyncio_Future__asyncio_future_blocking_set(PyObject *self, PyObject *value, vo { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_asyncio_future_blocking' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _asyncio_Future__asyncio_future_blocking_set_impl((FutureObj *)self, value); Py_END_CRITICAL_SECTION(); @@ -635,6 +641,12 @@ _asyncio_Future__log_traceback_set(PyObject *self, PyObject *value, void *Py_UNU { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_log_traceback' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _asyncio_Future__log_traceback_set_impl((FutureObj *)self, value); Py_END_CRITICAL_SECTION(); @@ -810,6 +822,12 @@ _asyncio_Future__cancel_message_set(PyObject *self, PyObject *value, void *Py_UN { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_cancel_message' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _asyncio_Future__cancel_message_set_impl((FutureObj *)self, value); Py_END_CRITICAL_SECTION(); @@ -1002,6 +1020,12 @@ _asyncio_Task__log_destroy_pending_set(PyObject *self, PyObject *value, void *Py { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_log_destroy_pending' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _asyncio_Task__log_destroy_pending_set_impl((TaskObj *)self, value); Py_END_CRITICAL_SECTION(); @@ -2234,4 +2258,4 @@ _asyncio_future_discard_from_awaited_by(PyObject *module, PyObject *const *args, exit: return return_value; } -/*[clinic end generated code: output=22e74568ff49f81f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=46d50c477614b57e input=a9049054013a1b77]*/ diff --git a/Modules/clinic/_ssl.c.h b/Modules/clinic/_ssl.c.h index e337ed2390a1fc4..62d52fc5f1aa5dd 100644 --- a/Modules/clinic/_ssl.c.h +++ b/Modules/clinic/_ssl.c.h @@ -386,6 +386,12 @@ _ssl__SSLSocket_context_set(PyObject *self, PyObject *value, void *Py_UNUSED(con { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'context' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLSocket_context_set_impl((PySSLSocket *)self, value); Py_END_CRITICAL_SECTION(); @@ -509,6 +515,12 @@ _ssl__SSLSocket_owner_set(PyObject *self, PyObject *value, void *Py_UNUSED(conte { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'owner' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLSocket_owner_set_impl((PySSLSocket *)self, value); Py_END_CRITICAL_SECTION(); @@ -914,6 +926,12 @@ _ssl__SSLSocket_session_set(PyObject *self, PyObject *value, void *Py_UNUSED(con { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'session' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLSocket_session_set_impl((PySSLSocket *)self, value); Py_END_CRITICAL_SECTION(); @@ -1342,6 +1360,12 @@ _ssl__SSLContext_verify_mode_set(PyObject *self, PyObject *value, void *Py_UNUSE { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'verify_mode' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_verify_mode_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1392,6 +1416,12 @@ _ssl__SSLContext_verify_flags_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'verify_flags' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_verify_flags_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1443,6 +1473,12 @@ _ssl__SSLContext_minimum_version_set(PyObject *self, PyObject *value, void *Py_U { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'minimum_version' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_minimum_version_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1494,6 +1530,12 @@ _ssl__SSLContext_maximum_version_set(PyObject *self, PyObject *value, void *Py_U { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'maximum_version' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_maximum_version_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1551,6 +1593,12 @@ _ssl__SSLContext_num_tickets_set(PyObject *self, PyObject *value, void *Py_UNUSE { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'num_tickets' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_num_tickets_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1633,6 +1681,12 @@ _ssl__SSLContext_options_set(PyObject *self, PyObject *value, void *Py_UNUSED(co { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'options' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_options_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1683,6 +1737,12 @@ _ssl__SSLContext__host_flags_set(PyObject *self, PyObject *value, void *Py_UNUSE { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute '_host_flags' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext__host_flags_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -1733,6 +1793,12 @@ _ssl__SSLContext_check_hostname_set(PyObject *self, PyObject *value, void *Py_UN { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'check_hostname' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_check_hostname_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -2265,6 +2331,12 @@ _ssl__SSLContext_sni_callback_set(PyObject *self, PyObject *value, void *Py_UNUS { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'sni_callback' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = _ssl__SSLContext_sni_callback_set_impl((PySSLContext *)self, value); Py_END_CRITICAL_SECTION(); @@ -3326,4 +3398,4 @@ _ssl_enum_crls(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObje #ifndef _SSL_ENUM_CRLS_METHODDEF #define _SSL_ENUM_CRLS_METHODDEF #endif /* !defined(_SSL_ENUM_CRLS_METHODDEF) */ -/*[clinic end generated code: output=aef2e74b706c6106 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=3a5bdd8db17e32b1 input=a9049054013a1b77]*/ diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c index d009877dc09fac0..055fedc3ddfb034 100644 --- a/Objects/bytearrayobject.c +++ b/Objects/bytearrayobject.c @@ -681,8 +681,11 @@ bytearray_setslice(PyByteArrayObject *self, Py_ssize_t lo, Py_ssize_t hi, bytes = vbytes.buf; } + // gh-153578: __buffer__() may have resized self; re-clamp both bounds. if (lo < 0) lo = 0; + else if (lo > Py_SIZE(self)) + lo = Py_SIZE(self); if (hi < lo) hi = lo; if (hi > Py_SIZE(self)) diff --git a/Objects/clinic/frameobject.c.h b/Objects/clinic/frameobject.c.h index 327896f4b97c684..7b8dab1e015a6b0 100644 --- a/Objects/clinic/frameobject.c.h +++ b/Objects/clinic/frameobject.c.h @@ -265,6 +265,12 @@ frame_trace_opcodes_set(PyObject *self, PyObject *value, void *Py_UNUSED(context { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'f_trace_opcodes' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = frame_trace_opcodes_set_impl((PyFrameObject *)self, value); Py_END_CRITICAL_SECTION(); @@ -290,6 +296,12 @@ frame_lineno_set(PyObject *self, PyObject *value, void *Py_UNUSED(context)) { int return_value; + if (value == NULL) { + PyErr_Format(PyExc_AttributeError, + "attribute 'f_lineno' of '%.100s' objects cannot be deleted", + Py_TYPE(self)->tp_name); + return -1; + } Py_BEGIN_CRITICAL_SECTION(self); return_value = frame_lineno_set_impl((PyFrameObject *)self, value); Py_END_CRITICAL_SECTION(); @@ -433,4 +445,4 @@ frame___sizeof__(PyObject *self, PyObject *Py_UNUSED(ignored)) return return_value; } -/*[clinic end generated code: output=74abf652547c0c11 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=a42421e56faa7a80 input=a9049054013a1b77]*/ diff --git a/Objects/exceptions.c b/Objects/exceptions.c index fb546ad2673576d..cc3e03baa4c7dfe 100644 --- a/Objects/exceptions.c +++ b/Objects/exceptions.c @@ -346,12 +346,13 @@ BaseException_args_get_impl(PyBaseExceptionObject *self) /*[clinic input] @critical_section @setter +@deleter BaseException.args [clinic start generated code]*/ static int BaseException_args_set_impl(PyBaseExceptionObject *self, PyObject *value) -/*[clinic end generated code: output=331137e11d8f9e80 input=2400047ea5970a84]*/ +/*[clinic end generated code: output=331137e11d8f9e80 input=177ad350c8b45219]*/ { PyObject *seq; if (value == NULL) { @@ -385,13 +386,14 @@ BaseException___traceback___get_impl(PyBaseExceptionObject *self) /*[clinic input] @critical_section @setter +@deleter BaseException.__traceback__ [clinic start generated code]*/ static int BaseException___traceback___set_impl(PyBaseExceptionObject *self, PyObject *value) -/*[clinic end generated code: output=a82c86d9f29f48f0 input=12676035676badad]*/ +/*[clinic end generated code: output=a82c86d9f29f48f0 input=53a1df586023d786]*/ { if (value == NULL) { PyErr_SetString(PyExc_TypeError, "__traceback__ may not be deleted"); @@ -430,13 +432,14 @@ BaseException___context___get_impl(PyBaseExceptionObject *self) /*[clinic input] @critical_section @setter +@deleter BaseException.__context__ [clinic start generated code]*/ static int BaseException___context___set_impl(PyBaseExceptionObject *self, PyObject *value) -/*[clinic end generated code: output=b4cb52dcca1da3bd input=c0971adf47fa1858]*/ +/*[clinic end generated code: output=b4cb52dcca1da3bd input=fe79e7c0a0854004]*/ { if (value == NULL) { PyErr_SetString(PyExc_TypeError, "__context__ may not be deleted"); @@ -473,13 +476,14 @@ BaseException___cause___get_impl(PyBaseExceptionObject *self) /*[clinic input] @critical_section @setter +@deleter BaseException.__cause__ [clinic start generated code]*/ static int BaseException___cause___set_impl(PyBaseExceptionObject *self, PyObject *value) -/*[clinic end generated code: output=6161315398aaf541 input=e1b403c0bde3f62a]*/ +/*[clinic end generated code: output=6161315398aaf541 input=3fdd9a0d1674abc9]*/ { if (value == NULL) { PyErr_SetString(PyExc_TypeError, "__cause__ may not be deleted"); diff --git a/Objects/frameobject.c b/Objects/frameobject.c index c50cbeaada3c406..5889cdaf2aa1652 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -1651,10 +1651,6 @@ frame_lineno_set_impl(PyFrameObject *self, PyObject *value) /*[clinic end generated code: output=e64c86ff6be64292 input=36ed3c896b27fb91]*/ { PyCodeObject *code = _PyFrame_GetCode(self->f_frame); - if (value == NULL) { - PyErr_SetString(PyExc_AttributeError, "cannot delete attribute"); - return -1; - } /* f_lineno must be an integer. */ if (!PyLong_CheckExact(value)) { PyErr_SetString(PyExc_ValueError, @@ -1868,12 +1864,13 @@ frame_trace_get_impl(PyFrameObject *self) @permit_long_summary @critical_section @setter +@deleter frame.f_trace as frame_trace [clinic start generated code]*/ static int frame_trace_set_impl(PyFrameObject *self, PyObject *value) -/*[clinic end generated code: output=d6fe08335cf76ae4 input=e57380734815dac5]*/ +/*[clinic end generated code: output=d6fe08335cf76ae4 input=9fb7a5805196eae2]*/ { if (value == Py_None) { value = NULL; diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 0c1fab7f6d33a8a..0481adadf668f8d 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -926,12 +926,13 @@ function___annotate___get_impl(PyFunctionObject *self) /*[clinic input] @critical_section @setter +@deleter function.__annotate__ [clinic start generated code]*/ static int function___annotate___set_impl(PyFunctionObject *self, PyObject *value) -/*[clinic end generated code: output=05b7dfc07ada66cd input=eb6225e358d97448]*/ +/*[clinic end generated code: output=05b7dfc07ada66cd input=4bcfad0bdcfec768]*/ { if (value == NULL) { PyErr_SetString(PyExc_TypeError, @@ -980,12 +981,13 @@ function___annotations___get_impl(PyFunctionObject *self) /*[clinic input] @critical_section @setter +@deleter function.__annotations__ [clinic start generated code]*/ static int function___annotations___set_impl(PyFunctionObject *self, PyObject *value) -/*[clinic end generated code: output=a61795d4a95eede4 input=5302641f686f0463]*/ +/*[clinic end generated code: output=a61795d4a95eede4 input=71f6a58c00ac6745]*/ { if (value == Py_None) value = NULL; @@ -1025,12 +1027,13 @@ function___type_params___get_impl(PyFunctionObject *self) /*[clinic input] @critical_section @setter +@deleter function.__type_params__ [clinic start generated code]*/ static int function___type_params___set_impl(PyFunctionObject *self, PyObject *value) -/*[clinic end generated code: output=038b4cda220e56fb input=3862fbd4db2b70e8]*/ +/*[clinic end generated code: output=038b4cda220e56fb input=c0e33abc5901a2f5]*/ { /* Not legal to del f.__type_params__ or to set it to anything * other than a tuple object. */ diff --git a/PC/pyconfig.h b/PC/pyconfig.h index 2381ed3772b109e..22c3ccc05bb004e 100644 --- a/PC/pyconfig.h +++ b/PC/pyconfig.h @@ -317,39 +317,33 @@ Py_NO_ENABLE_SHARED to find out. Also support MS_NO_COREDLL for b/w compat */ /* All windows compilers that use this header support __declspec */ #define HAVE_DECLSPEC_DLL -/* For an MSVC DLL, we can nominate the .lib files used by extensions */ -#ifdef MS_COREDLL -# if !defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_BUILTIN) - /* not building the core - must be an ext */ -# if defined(_MSC_VER) && !defined(Py_NO_LINK_LIB) - /* So MSVC users need not specify the .lib - file in their Makefile */ - /* Define Py_NO_LINK_LIB to build extension disabling pragma - based auto-linking. - This is relevant when using build-system generator (e.g CMake) where - the linking is explicitly handled */ -# if defined(Py_GIL_DISABLED) -# if defined(Py_DEBUG) -# pragma comment(lib,"python316t_d.lib") -# elif defined(Py_LIMITED_API) || defined(Py_TARGET_ABI3T) -# pragma comment(lib,"python3t.lib") -# else -# pragma comment(lib,"python316t.lib") -# endif /* Py_DEBUG */ -# else /* Py_GIL_DISABLED */ -# if defined(Py_DEBUG) -# pragma comment(lib,"python316_d.lib") -# elif defined(Py_TARGET_ABI3T) -# pragma comment(lib,"python3t.lib") -# elif defined(Py_LIMITED_API) -# pragma comment(lib,"python3.lib") -# else -# pragma comment(lib,"python316.lib") -# endif /* Py_DEBUG */ -# endif /* Py_GIL_DISABLED */ -# endif /* _MSC_VER && !Py_NO_LINK_LIB */ -# endif /* Py_BUILD_CORE */ -#endif /* MS_COREDLL */ +/* Automatic linking of extension python3x.lib files for MSVC DLLs. + This lets MSVC users build extensions without manually specifying .lib files. + Define Py_NO_LINK_LIB to disable this behavior. */ +#if !defined(Py_NO_LINK_LIB) \ + && defined(_MSC_VER) && defined(Py_ENABLE_SHARED) \ + && !defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_BUILTIN) + /* not building the core - must be an ext */ +# if defined(Py_GIL_DISABLED) +# if defined(Py_DEBUG) +# pragma comment(lib,"python316t_d.lib") +# elif defined(Py_LIMITED_API) || defined(Py_TARGET_ABI3T) +# pragma comment(lib,"python3t.lib") +# else +# pragma comment(lib,"python316t.lib") +# endif /* Py_DEBUG */ +# else +# if defined(Py_DEBUG) +# pragma comment(lib,"python316_d.lib") +# elif defined(Py_TARGET_ABI3T) +# pragma comment(lib,"python3t.lib") +# elif defined(Py_LIMITED_API) +# pragma comment(lib,"python3.lib") +# else +# pragma comment(lib,"python316.lib") +# endif /* Py_DEBUG */ +# endif /* Py_GIL_DISABLED */ +#endif #ifdef MS_WIN64 /* maintain "win32" sys.platform for backward compatibility of Python code, diff --git a/Python/traceback.c b/Python/traceback.c index 5bfa28f9c7dc8b6..fe6a465bc64cc94 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -176,12 +176,13 @@ tb_lineno_get(PyObject *op, void *Py_UNUSED(_)) /*[clinic input] @critical_section @setter +@deleter traceback.tb_next [clinic start generated code]*/ static int traceback_tb_next_set_impl(PyTracebackObject *self, PyObject *value) -/*[clinic end generated code: output=d4868cbc48f2adac input=ce66367f85e3c443]*/ +/*[clinic end generated code: output=d4868cbc48f2adac input=936201ff689c5700]*/ { if (!value) { PyErr_Format(PyExc_TypeError, "can't delete tb_next attribute"); diff --git a/Tools/clinic/libclinic/clanguage.py b/Tools/clinic/libclinic/clanguage.py index a8473dba0512460..ab77e7ad6603cdc 100644 --- a/Tools/clinic/libclinic/clanguage.py +++ b/Tools/clinic/libclinic/clanguage.py @@ -13,7 +13,8 @@ from libclinic.function import ( Module, Class, Function, Parameter, ParamTuple, permute_optional_groups, - GETTER, SETTER, METHOD_INIT) + GETTER, METHOD_INIT, + ACCESSORS, SETTERS) from libclinic.converters import self_converter from libclinic.parse_args import ParseArgsCodeGen if TYPE_CHECKING: @@ -478,12 +479,12 @@ def render_function( full_name = f.full_name template_dict = {'full_name': full_name} template_dict['name'] = f.displayname - if f.kind in {GETTER, SETTER}: + if f.kind in ACCESSORS: template_dict['getset_name'] = f.c_basename.upper() template_dict['getset_basename'] = f.c_basename if f.kind is GETTER: template_dict['c_basename'] = f.c_basename + "_get" - elif f.kind is SETTER: + else: template_dict['c_basename'] = f.c_basename + "_set" # Implicitly add the setter value parameter. data.impl_parameters.append("PyObject *value") @@ -498,7 +499,7 @@ def render_function( for converter in converters: converter.set_template_dict(template_dict) - if f.kind not in {SETTER, METHOD_INIT}: + if f.kind not in SETTERS | {METHOD_INIT}: f.return_converter.render(f, data) template_dict['impl_return_type'] = f.return_converter.type diff --git a/Tools/clinic/libclinic/cli.py b/Tools/clinic/libclinic/cli.py index c66084cf3144826..290fc3a6e59408a 100644 --- a/Tools/clinic/libclinic/cli.py +++ b/Tools/clinic/libclinic/cli.py @@ -89,10 +89,15 @@ def parse_file( filename=filename, limited_capi=limited_capi, writer=writer) + index = len(writer.files) cooked = clinic.parse(raw) - writer.write(output, cooked) + files = writer.files[index:] + writer.update_times(output, + [fn for fn, _ in files if fn != output], + any(changed for _, changed in files)) + def create_cli() -> argparse.ArgumentParser: cmdline = argparse.ArgumentParser( diff --git a/Tools/clinic/libclinic/converters.py b/Tools/clinic/libclinic/converters.py index 76091a9eedc1bff..5539bd2e12e35f5 100644 --- a/Tools/clinic/libclinic/converters.py +++ b/Tools/clinic/libclinic/converters.py @@ -8,7 +8,7 @@ from libclinic.function import ( Function, Parameter, CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, - GETTER, SETTER) + ACCESSORS) from libclinic.codegen import CRenderData, TemplateDict from libclinic.converter import ( CConverter, legacy_converters, add_legacy_c_converter) @@ -1124,7 +1124,7 @@ def correct_name_for_self( f: Function, parser: bool = False ) -> tuple[str, str]: - if f.kind in {CALLABLE, METHOD_INIT, GETTER, SETTER}: + if f.kind in {CALLABLE, METHOD_INIT} | ACCESSORS: if f.cls: return "PyObject *", "self" return "PyObject *", "module" diff --git a/Tools/clinic/libclinic/dsl_parser.py b/Tools/clinic/libclinic/dsl_parser.py index 4dcbc815cc6f25b..a6b1d2bed5e5dee 100644 --- a/Tools/clinic/libclinic/dsl_parser.py +++ b/Tools/clinic/libclinic/dsl_parser.py @@ -18,7 +18,7 @@ Module, Class, Function, Parameter, FunctionKind, CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW, - GETTER, SETTER) + ACCESSORS, SETTERS) from libclinic.converter import ( converters, legacy_converters) from libclinic.converters import ( @@ -447,21 +447,31 @@ def at_disable(self, *args: str) -> None: def at_getter(self) -> None: match self.kind: + case FunctionKind.CALLABLE: + self.kind = FunctionKind.GETTER case FunctionKind.GETTER: fail("Cannot apply @getter twice to the same function!") - case FunctionKind.SETTER: - fail("Cannot apply both @getter and @setter to the same function!") case _: - self.kind = FunctionKind.GETTER + fail("Can't set @getter, function is not a normal callable") def at_setter(self) -> None: match self.kind: - case FunctionKind.SETTER: + case FunctionKind.CALLABLE: + self.kind = FunctionKind.SETTER + case FunctionKind.SETTER | FunctionKind.SETTER_AND_DELETER: fail("Cannot apply @setter twice to the same function!") - case FunctionKind.GETTER: - fail("Cannot apply both @getter and @setter to the same function!") case _: - self.kind = FunctionKind.SETTER + fail("Can't set @setter, function is not a normal callable") + + def at_deleter(self) -> None: + match self.kind: + case FunctionKind.SETTER: + # The setter is called with NULL to delete the attribute. + self.kind = FunctionKind.SETTER_AND_DELETER + case FunctionKind.SETTER_AND_DELETER: + fail("Cannot apply @deleter twice to the same function!") + case _: + fail("Can't set @deleter, @setter is not applied") def at_staticmethod(self) -> None: if self.kind is not CALLABLE: @@ -592,7 +602,7 @@ def normalize_function_kind(self, fullname: str) -> None: fail(f"{name!r} must be a normal method; got '{self.kind}'!") if name == '__new__' and (self.kind is not CLASS_METHOD or not cls): fail("'__new__' must be a class method!") - if self.kind in {GETTER, SETTER} and not cls: + if self.kind in ACCESSORS and not cls: fail("@getter and @setter must be methods") # Normalise self.kind. @@ -605,8 +615,8 @@ def resolve_return_converter( self, full_name: str, forced_converter: str ) -> CReturnConverter: if forced_converter: - if self.kind in {GETTER, SETTER}: - fail(f"@{self.kind.name.lower()} method cannot define a return type") + if self.kind in ACCESSORS: + fail("@getter and @setter methods cannot define a return type") if self.kind is METHOD_INIT: fail("__init__ methods cannot define a return type") ast_input = f"def x() -> {forced_converter}: pass" @@ -626,7 +636,7 @@ def resolve_return_converter( except ValueError: fail(f"Badly formed annotation for {full_name!r}: {forced_converter!r}") - if self.kind in {METHOD_INIT, SETTER}: + if self.kind in {METHOD_INIT} | SETTERS: return int_return_converter() return CReturnConverter() @@ -732,6 +742,22 @@ def state_modulename_name(self, line: str) -> None: self.next(self.state_parameters_start) def add_function(self, func: Function) -> None: + if func.kind in ACCESSORS: + # The accessors of the same attribute are rendered into a single + # PyGetSetDef entry, which is identified by the C basename, so + # they must share it. + for other in (func.cls or func.module).functions: + if (other.kind in ACCESSORS + and other.full_name == func.full_name): + if (other.kind is func.kind + or {other.kind, func.kind} <= SETTERS): + kind = 'setter' if func.kind in SETTERS else 'getter' + fail(f"Cannot apply @{kind} to " + f"{func.full_name!r} twice") + if other.c_basename != func.c_basename: + fail(f"The accessors of {func.full_name!r} " + f"must have the same C basename") + # Insert a self converter automatically. tp, name = correct_name_for_self(func) if func.cls and tp == "PyObject *": @@ -814,9 +840,8 @@ def state_parameters_start(self, line: str) -> None: return self.next(self.state_function_docstring, line) assert self.function is not None - if self.function.kind in {GETTER, SETTER}: - getset = self.function.kind.name.lower() - fail(f"@{getset} methods cannot define parameters") + if self.function.kind in ACCESSORS: + fail("@getter and @setter methods cannot define parameters") self.parameter_continuation = '' return self.next(self.state_parameter, line) @@ -1358,7 +1383,7 @@ def format_docstring_signature( lines.append(f.displayname) if f.forced_text_signature: lines.append(f.forced_text_signature) - elif f.kind in {GETTER, SETTER}: + elif f.kind in ACCESSORS: # @getter and @setter do not need signatures like a method or a function. return '' else: @@ -1541,7 +1566,7 @@ def format_docstring(self) -> str: assert self.function is not None f = self.function # For the following special cases, it does not make sense to render a docstring. - if f.kind in {METHOD_INIT, METHOD_NEW, GETTER, SETTER} and not f.docstring: + if f.kind in {METHOD_INIT, METHOD_NEW} | ACCESSORS and not f.docstring: return f.docstring # Enforce the summary line! diff --git a/Tools/clinic/libclinic/function.py b/Tools/clinic/libclinic/function.py index 325633eb010608f..cad673045d1c26d 100644 --- a/Tools/clinic/libclinic/function.py +++ b/Tools/clinic/libclinic/function.py @@ -60,6 +60,7 @@ class FunctionKind(enum.Enum): METHOD_NEW = enum.auto() GETTER = enum.auto() SETTER = enum.auto() + SETTER_AND_DELETER = enum.auto() @functools.cached_property def new_or_init(self) -> bool: @@ -76,6 +77,12 @@ def __repr__(self) -> str: METHOD_NEW: Final = FunctionKind.METHOD_NEW GETTER: Final = FunctionKind.GETTER SETTER: Final = FunctionKind.SETTER +SETTER_AND_DELETER: Final = FunctionKind.SETTER_AND_DELETER + +# The kinds which implement the setter of an entry of PyGetSetDef. +SETTERS: Final = frozenset({SETTER, SETTER_AND_DELETER}) +# The kinds which implement an entry of PyGetSetDef. +ACCESSORS: Final = SETTERS | {GETTER} @dc.dataclass(repr=False) @@ -161,7 +168,7 @@ def methoddef_flags(self) -> str | None: case FunctionKind.STATIC_METHOD: flags.append('METH_STATIC') case _ as kind: - acceptable_kinds = {FunctionKind.CALLABLE, FunctionKind.GETTER, FunctionKind.SETTER} + acceptable_kinds = {FunctionKind.CALLABLE} | ACCESSORS assert kind in acceptable_kinds, f"unknown kind: {kind!r}" if self.coexist: flags.append('METH_COEXIST') diff --git a/Tools/clinic/libclinic/parse_args.py b/Tools/clinic/libclinic/parse_args.py index 0e99a89d74d7241..b08b949028205d2 100644 --- a/Tools/clinic/libclinic/parse_args.py +++ b/Tools/clinic/libclinic/parse_args.py @@ -5,7 +5,8 @@ from libclinic import fail, warn from libclinic.function import ( Function, Parameter, - GETTER, SETTER, METHOD_NEW) + GETTER, SETTER, METHOD_NEW, + ACCESSORS, SETTERS) from libclinic.converter import CConverter from libclinic.converters import ( defining_class_converter, object_converter, self_converter) @@ -188,6 +189,21 @@ def declare_parser( #define {methoddef_name} #endif /* !defined({methoddef_name}) */ """) +GETSETDEF_PROTOTYPE_IFNDEF: Final[str] = libclinic.normalize_snippet(""" + #ifndef {getset_name}_GETSETDEF + #define {getset_name}_GETSETDEF + #endif /* !defined({getset_name}_GETSETDEF) */ +""") +# The setter is called with NULL to delete the attribute. Unless @deleter is +# applied to it, deletion is rejected before the implementation is called. +SETTER_PREAMBLE: Final[str] = libclinic.normalize_snippet(""" + if (value == NULL) {{ + PyErr_Format(PyExc_AttributeError, + "attribute '{name}' of '%.100s' objects cannot be deleted", + Py_TYPE({self_name})->tp_name); + return -1; + }} +""", indent=4) class ParseArgsCodeGen: @@ -328,7 +344,7 @@ def select_prototypes(self) -> None: self.methoddef_define = GETTERDEF_PROTOTYPE_DEFINE if self.func.docstring: self.docstring_definition = GETSET_DOCSTRING_PROTOTYPE_STRVAR - elif self.func.kind is SETTER: + elif self.func.kind in SETTERS: if self.func.docstring: fail("docstrings are only supported for @getter, not @setter") self.return_value_declaration = "int {parser_retval};" @@ -387,9 +403,12 @@ def parse_no_args(self) -> None: if self.func.kind is GETTER: self.parser_prototype = PARSER_PROTOTYPE_GETTER parser_code = [] - elif self.func.kind is SETTER: + elif self.func.kind in SETTERS: self.parser_prototype = PARSER_PROTOTYPE_SETTER - parser_code = [] + if self.func.kind is SETTER: + parser_code = [SETTER_PREAMBLE] + else: + parser_code = [] elif not self.requires_defining_class: # no self.parameters, METH_NOARGS self.flags = "METH_NOARGS" @@ -921,7 +940,10 @@ def process_methoddef(self, clang: CLanguage) -> None: self.cpp_endif = "#endif /* " + conditional + " */" if self.methoddef_define and self.codegen.add_ifndef_symbol(self.func.full_name): - self.methoddef_ifndef = METHODDEF_PROTOTYPE_IFNDEF + if self.func.kind in ACCESSORS: + self.methoddef_ifndef = GETSETDEF_PROTOTYPE_IFNDEF + else: + self.methoddef_ifndef = METHODDEF_PROTOTYPE_IFNDEF def finalize(self, clang: CLanguage) -> None: # add ';' to the end of self.parser_prototype and self.impl_prototype diff --git a/Tools/clinic/libclinic/utils.py b/Tools/clinic/libclinic/utils.py index 8fc8748f0f9ae10..01015ff1237656b 100644 --- a/Tools/clinic/libclinic/utils.py +++ b/Tools/clinic/libclinic/utils.py @@ -5,6 +5,7 @@ import os import re import string +from collections.abc import Iterable from typing import Literal, Final @@ -17,11 +18,14 @@ def read_file(filename: str) -> str | None: return None -def write_file(filename: str, new_contents: str) -> None: - """Write new content to file, iff the content changed.""" +def write_file(filename: str, new_contents: str) -> bool: + """Write new content to file, iff the content changed. + + Return True if the file was written. + """ if read_file(filename) == new_contents: # no change: avoid modifying the file modification time - return + return False # Atomic write using a temporary file and os.replace() filename_new = f"{filename}.new" with open(filename_new, "w", encoding="utf-8") as fp: @@ -31,6 +35,7 @@ def write_file(filename: str, new_contents: str) -> None: except: os.unlink(filename_new) raise + return True @dc.dataclass(slots=True, frozen=True) @@ -50,6 +55,8 @@ class FileWriter: dry_run: bool = False changes: list[FileChange] = dc.field(default_factory=list) + # (filename, changed) for every file which was passed to write(). + files: list[tuple[str, bool]] = dc.field(default_factory=list) def makedirs(self, dirname: str) -> None: if not self.dry_run: @@ -61,12 +68,34 @@ def makedirs(self, dirname: str) -> None: def write(self, filename: str, new_contents: str) -> None: if not self.dry_run: - write_file(filename, new_contents) + changed = write_file(filename, new_contents) + else: + old_contents = read_file(filename) + changed = old_contents != new_contents + if changed: + self.changes.append( + FileChange(filename, old_contents, new_contents)) + self.files.append((filename, changed)) + + def update_times(self, source: str, generated: Iterable[str], + changed: bool) -> None: + """Keep the generated files newer than the source file. + + The build system does not know that the source file depends on + the files generated from it, so the source file is touched to + force its recompilation. + """ + if self.dry_run: return - old_contents = read_file(filename) - if old_contents != new_contents: - self.changes.append( - FileChange(filename, old_contents, new_contents)) + if changed: + os.utime(source) + for filename in generated: + os.utime(filename) + else: + mtime = os.stat(source).st_mtime_ns + for filename in generated: + if os.stat(filename).st_mtime_ns <= mtime: + os.utime(filename) def compute_checksum(input_: str, length: int | None = None) -> str: