From bf076f10c503749521d176b9b757fe5b3088fe63 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Fri, 16 Oct 2026 12:58:31 +0900 Subject: [PATCH 1/3] Fix deprecated PyO3 code input import in benchmarks Use the canonical code module path introduced in PyO3 0.30 without suppressing deprecation warnings. Assisted-by: OpenAI Codex:model version unavailable --- benches/execution.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/execution.rs b/benches/execution.rs index a09ab9c7890..c0c783f24ed 100644 --- a/benches/execution.rs +++ b/benches/execution.rs @@ -7,7 +7,7 @@ use criterion::{ Bencher, BenchmarkGroup, BenchmarkId, Criterion, Throughput, criterion_group, criterion_main, measurement::WallTime, }; -use pyo3::types::{PyCode, PyCodeInput, PyCodeMethods, PyDict, PyDictMethods}; +use pyo3::types::{PyCode, PyCodeMethods, PyDict, PyDictMethods, code::PyCodeInput}; use rustpython_compiler::Mode; use rustpython_vm::{Interpreter, PyResult, Settings}; use std::{collections::HashMap, path::Path}; From 4a6dfbe6c69553f4a80fcfb6486f132d62429d8c Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 16 Oct 2026 13:29:42 +0900 Subject: [PATCH 2/3] Align runtime metadata and annotation cache contracts with Python 3.15 Preserve function name identity, implicit wrapper metadata and slice generic aliases. Match annotation dictionary and reentrant publication contracts, releasing displaced Python owners outside locks in the required phases. Refresh the matching canonical annotation tests through update_lib. Assisted-by: OpenAI Codex: gpt-6.1-sol --- Lib/test/test_type_annotations.py | 34 +++---- crates/vm/src/builtins/function.rs | 62 +++++++------ crates/vm/src/builtins/module.rs | 10 ++- crates/vm/src/builtins/slice.rs | 10 +-- crates/vm/src/builtins/type.rs | 138 ++++++++++++++--------------- 5 files changed, 126 insertions(+), 128 deletions(-) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 2b95cabf2e4..54db123a5dc 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -310,7 +310,7 @@ def check_annotations(self, f): print(f.__annotations__) f.__annotate__ = lambda x: 42 - with self.assertRaisesRegex(TypeError, r"__annotate__ returned non-dict of type 'int'"): + with self.assertRaisesRegex(TypeError, r"__annotate__\(\) must return a dict, not int"): print(f.__annotations__) f.__annotate__ = lambda x: {"x": x} @@ -783,21 +783,6 @@ def test_non_name_annotations(self): class RegressionTests(unittest.TestCase): - # gh-154902 - def test_conditional_annotations_rebound(self): - # user code can rebind __conditional_annotations__ to any object - lefts = ("__conditional_annotations__", - 'globals()["__conditional_annotations__"]') - values = ("0", "{}", "[]", "''", "object()", "frozenset()") - for left, value in itertools.product(lefts, values): - with self.subTest(left=left, value=value): - code = f""" - {left} = {value} - x: int - """ - with self.assertRaises(TypeError): - run_code(code) - # gh-132479 def test_complex_comprehension_inlining(self): # Test that the various repro cases from the issue don't crash @@ -871,9 +856,9 @@ def method(self, x: int): """ ns = run_code(code) method = ns["Outer"].method + self.assertEqual(method.__annotate__.__qualname__, "Outer.method.__annotate__") self.assertEqual(ns["f"].__annotate__.__qualname__, "f.__annotate__") self.assertEqual(ns["f"]().__annotate__.__qualname__, "f..nested.__annotate__") - self.assertEqual(method.__annotate__.__qualname__, "Outer.method.__annotate__") self.assertEqual(ns["Outer"].__annotate__.__qualname__, "Outer.__annotate__") # gh-138349 @@ -912,3 +897,18 @@ class Generic: mod = build_module(code) annos = mod.__annotations__ self.assertEqual(annos, {"annotated_name": 0}) + + # gh-154902 + def test_conditional_annotations_rebound(self): + # user code can rebind __conditional_annotations__ to any object + lefts = ("__conditional_annotations__", + 'globals()["__conditional_annotations__"]') + values = ("0", "{}", "[]", "''", "object()", "frozenset()") + for left, value in itertools.product(lefts, values): + with self.subTest(left=left, value=value): + code = f""" + {left} = {value} + x: int + """ + with self.assertRaises(TypeError): + run_code(code) diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index 80127a76fe7..b7629cf205c 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -182,7 +182,7 @@ impl PyFunction { globals: PyRef, vm: &VirtualMachine, ) -> PyResult { - let name = PyMutex::new(code.obj_name.to_owned()); + let name = PyMutex::new(code.co_name.clone()); let module = vm.unwrap_or_none(globals.inner_getitem_opt(identifier!(vm, __name__), vm)?); let builtins = globals .inner_getitem_opt(identifier!(vm, __builtins__), vm)? @@ -1095,24 +1095,28 @@ impl Py { // Release locks before calling __annotate__ to avoid deadlock if let Some(annotate_fn) = annotate_fn { let one = vm.ctx.new_int(1); - let ann_dict = annotate_fn.call((one,), vm)?; - let ann_dict = ann_dict - .downcast::() - .map_err(|obj| { - vm.new_type_error(format!( - "__annotate__ returned non-dict of type '{}'", - obj.class().name() - )) - })?; + let result = annotate_fn.call((one,), vm); + drop(annotate_fn); + let ann_dict = match result?.downcast::() { + Ok(dict) => dict, + Err(obj) => { + return Err(vm.new_type_error(format!( + "__annotate__() must return a dict, not {}", + obj.class().fully_qualified_name(vm)? + ))); + } + }; // Cache the result - *self.annotations.lock() = Some(ann_dict.clone()); + let old_annotations = self.annotations.lock().replace(ann_dict.clone()); + drop(old_annotations); return Ok(ann_dict); } // No __annotate__ or not callable, create empty dict let new_dict = vm.ctx.new_dict(); - *self.annotations.lock() = Some(new_dict.clone()); + let old_annotations = self.annotations.lock().replace(new_dict.clone()); + drop(old_annotations); Ok(new_dict) } @@ -1122,23 +1126,19 @@ impl Py { value: PySetterValue>, vm: &VirtualMachine, ) -> PyResult<()> { - match value { + let annotations = match value { PySetterValue::Assign(Some(value)) => { let annotations = value.downcast::().map_err(|_| { vm.new_type_error("__annotations__ must be set to a dict object") })?; - *self.annotations.lock() = Some(annotations); - *self.annotate.lock() = None; - } - PySetterValue::Assign(None) => { - *self.annotations.lock() = None; - *self.annotate.lock() = None; - } - PySetterValue::Delete => { - // del only clears cached annotations; __annotate__ is preserved - *self.annotations.lock() = None; + Some(annotations) } - } + PySetterValue::Assign(None) | PySetterValue::Delete => None, + }; + let old_annotations = core::mem::replace(&mut *self.annotations.lock(), annotations); + drop(old_annotations); + let old_annotate = self.annotate.lock().take(); + drop(old_annotate); Ok(()) } @@ -1171,8 +1171,6 @@ impl Py { if !value.is_callable() { return Err(vm.new_type_error("__annotate__ must be callable or None")); } - // Clear cached __annotations__ when __annotate__ is set - *self.annotations.lock() = None; Some(value) } PySetterValue::Assign(None) => None, @@ -1180,7 +1178,13 @@ impl Py { return Err(vm.new_type_error("__annotate__ cannot be deleted")); } }; - *self.annotate.lock() = annotate; + let clear_annotations = annotate.is_some(); + let old_annotate = core::mem::replace(&mut *self.annotate.lock(), annotate); + drop(old_annotate); + if clear_annotations { + let old_annotations = self.annotations.lock().take(); + drop(old_annotations); + } Ok(()) } @@ -1288,7 +1292,7 @@ pub struct PyFunctionNewArgs { #[pyarg(positional)] globals: PyDictRef, #[pyarg(any, optional, error_msg = "arg 3 (name) must be None or string")] - name: OptionalArg, + name: Option, #[pyarg(any, optional, error_msg = "arg 4 (defaults) must be None or tuple")] argdefs: Option, #[pyarg(any, optional, error_msg = "arg 5 (closure) must be None or tuple")] @@ -1324,7 +1328,7 @@ impl Constructor for PyFunction { let mut func = Self::new(args.code.clone(), args.globals.clone().into(), vm)?; // Set function name if provided - if let Some(name) = args.name.into_option() { + if let Some(name) = args.name { *func.name.lock() = name; } // Now set additional attributes directly diff --git a/crates/vm/src/builtins/module.rs b/crates/vm/src/builtins/module.rs index e7f9e2bfc9f..e479cd54305 100644 --- a/crates/vm/src/builtins/module.rs +++ b/crates/vm/src/builtins/module.rs @@ -394,10 +394,12 @@ impl PyModule { // Call __annotate__(1) where 1 is FORMAT_VALUE let result = annotate.call((1i32,), vm)?; if !result.downcastable::() { - return Err(vm.new_type_error(format!( - "__annotate__ returned non-dict of type '{}'", - result.class().name() - ))); + let error = result.class().fully_qualified_name(vm).map(|name| { + vm.new_type_error(format!("__annotate__() must return a dict, not {name}")) + }); + drop(annotate); + drop(result); + return Err(error?); } result } else { diff --git a/crates/vm/src/builtins/slice.rs b/crates/vm/src/builtins/slice.rs index 8cc53fd16c1..d4aee6b28a1 100644 --- a/crates/vm/src/builtins/slice.rs +++ b/crates/vm/src/builtins/slice.rs @@ -187,9 +187,11 @@ impl PySlice { Ok((start, stop, step)) } +} - // TODO: Uncomment when Python adds __class_getitem__ to slice - // #[pyclassmethod] +#[pyclass(with(Comparable, Representable, Hashable))] +impl Py { + #[pyclassmethod] fn __class_getitem__( cls: PyTypeRef, args: PyObjectRef, @@ -197,10 +199,6 @@ impl PySlice { ) -> PyResult { PyGenericAlias::from_args(cls, args, vm) } -} - -#[pyclass(with(Comparable, Representable, Hashable))] -impl Py { #[pyslot] fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult { let slice: PySlice = match args.args.len() { diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 7a2e4010a22..8f1636d032a 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -1904,19 +1904,25 @@ impl Constructor for PyType { if let Some(f) = attributes.get_mut(identifier!(vm, __init_subclass__)) && f.class().is(vm.ctx.types.function_type) { - *f = PyClassMethod::from(f.clone()).into_pyobject(vm); + let callable = f.clone(); + *f = PyClassMethod::from(callable.clone()).into_pyobject(vm); + super::classmethod::functools_wraps(f, &callable, vm)?; } if let Some(f) = attributes.get_mut(identifier!(vm, __class_getitem__)) && f.class().is(vm.ctx.types.function_type) { - *f = PyClassMethod::from(f.clone()).into_pyobject(vm); + let callable = f.clone(); + *f = PyClassMethod::from(callable.clone()).into_pyobject(vm); + super::classmethod::functools_wraps(f, &callable, vm)?; } if let Some(f) = attributes.get_mut(identifier!(vm, __new__)) && f.class().is(vm.ctx.types.function_type) { - *f = PyStaticMethod::from(f.clone()).into_pyobject(vm); + let callable = f.clone(); + *f = PyStaticMethod::from(callable.clone()).into_pyobject(vm); + super::classmethod::functools_wraps(f, &callable, vm)?; } if let Some(globals) = crate::frame::current_globals() { @@ -2869,20 +2875,22 @@ impl Py { return Err(vm.new_type_error("__annotate__ must be callable or None")); } - let _prev_values = PyType::with_type_lock(vm, || { + let clear_annotations = !vm.is_none(&value); + let old_annotate = PyType::with_type_lock(vm, || { self.modified_inner(); - // Clear cached annotations only when setting to a new callable - let removed = if !vm.is_none(&value) { + self.attributes + .insert(identifier!(vm, __annotate_func__), value) + }); + drop(old_annotate); + // Clear cached annotations only when setting to a new callable. + if clear_annotations { + let old_annotations = PyType::with_type_lock(vm, || { + self.modified_inner(); self.attributes .remove(identifier!(vm, __annotations_cache__)) - } else { - None - }; - let prev = self - .attributes - .insert(identifier!(vm, __annotate_func__), value); - (removed, prev) - }); + }); + drop(old_annotations); + } Ok(()) } @@ -2931,10 +2939,10 @@ impl Py { let annotations = if annotate.is_callable() { // Call __annotate__(1) where 1 is FORMAT_VALUE let result = annotate.call((1i32,), vm)?; - if !result.class().is(vm.ctx.types.dict_type) { + if !result.downcastable::() { return Err(vm.new_type_error(format!( - "__annotate__ returned non-dict of type '{}'", - result.class().name() + "__annotate__() must return a dict, not {}", + result.class().fully_qualified_name(vm)? ))); } result @@ -2942,22 +2950,14 @@ impl Py { vm.ctx.new_dict().into() }; - let (result, _prev) = PyType::with_type_lock(vm, || { - if let Some(existing) = self.attributes.get(annotations_key) - && !existing.class().is(vm.ctx.types.getset_type) - { - return (existing, None); - } - if let Some(existing) = self.attributes.get(annotations_cache_key) { - return (existing, None); - } + drop(annotate); + let old_annotations = PyType::with_type_lock(vm, || { self.modified_inner(); - let prev = self - .attributes - .insert(annotations_cache_key, annotations.clone()); - (annotations, prev) + self.attributes + .insert(annotations_cache_key, annotations.clone()) }); - Ok(result) + drop(old_annotations); + Ok(annotations) } #[pygetset(setter)] @@ -2973,49 +2973,43 @@ impl Py { ))); } - let _prev_values = PyType::with_type_lock(vm, || { + let deleting = matches!(&value, PySetterValue::Delete); + let (has_annotations, old_annotations) = PyType::with_type_lock(vm, || { self.modified_inner(); let has_annotations = self.attributes.contains(identifier!(vm, __annotations__)); - - let mut prev = Vec::new(); - match value { - crate::function::PySetterValue::Assign(value) => { - let key = if has_annotations { - identifier!(vm, __annotations__) - } else { - identifier!(vm, __annotations_cache__) - }; - prev.extend(self.attributes.insert(key, value)); - if has_annotations { - prev.extend( - self.attributes - .remove(identifier!(vm, __annotations_cache__)), - ); - } - } - crate::function::PySetterValue::Delete => { - let removed = if has_annotations { - self.attributes.remove(identifier!(vm, __annotations__)) - } else { - self.attributes - .remove(identifier!(vm, __annotations_cache__)) - }; - if removed.is_none() { - return Err(vm.new_attribute_error("__annotations__")); - } - prev.extend(removed); - if has_annotations { - prev.extend( - self.attributes - .remove(identifier!(vm, __annotations_cache__)), - ); - } - } - } - prev.extend(self.attributes.remove(identifier!(vm, __annotate_func__))); - prev.extend(self.attributes.remove(identifier!(vm, __annotate__))); - Ok(prev) - })?; + let key = if has_annotations { + identifier!(vm, __annotations__) + } else { + identifier!(vm, __annotations_cache__) + }; + let old_annotations = match value { + crate::function::PySetterValue::Assign(value) => self.attributes.insert(key, value), + crate::function::PySetterValue::Delete => self.attributes.remove(key), + }; + (has_annotations, old_annotations) + }); + if deleting && old_annotations.is_none() { + return Err(vm.new_attribute_error("__annotations__")); + } + drop(old_annotations); + if has_annotations { + let old_cache = PyType::with_type_lock(vm, || { + self.modified_inner(); + self.attributes + .remove(identifier!(vm, __annotations_cache__)) + }); + drop(old_cache); + } + for key in [ + identifier!(vm, __annotate_func__), + identifier!(vm, __annotate__), + ] { + let old_annotate = PyType::with_type_lock(vm, || { + self.modified_inner(); + self.attributes.remove(key) + }); + drop(old_annotate); + } Ok(()) } From 3d8adae764d7db2cf593fa58b61f658043970ecd Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 16 Oct 2026 13:33:09 +0900 Subject: [PATCH 3/3] Refresh canonical generic alias coverage for slice Migrate the complete Python 3.15 test_genericalias file through update_lib. Preserve the full canonical type list and capability guards so normal test runs cover slice subscripting. Assisted-by: OpenAI Codex: gpt-6.1-sol --- Lib/test/test_genericalias.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 121d169dd07..7816775620b 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -17,7 +17,7 @@ from functools import partial, partialmethod, cached_property from graphlib import TopologicalSorter from logging import LoggerAdapter, StreamHandler -from mailbox import Mailbox, _PartialFile +from mailbox import Mailbox try: import ctypes except ImportError: @@ -100,7 +100,9 @@ class BaseTest(unittest.TestCase): """Test basics.""" - generic_types = [type, tuple, list, dict, set, frozenset, enumerate, memoryview, + generic_types = [type, tuple, list, dict, frozendict, + set, frozenset, enumerate, memoryview, + slice, defaultdict, deque, SequenceMatcher, dircmp, @@ -116,7 +118,7 @@ class BaseTest(unittest.TestCase): Iterable, Iterator, Reversible, Container, Collection, - Mailbox, _PartialFile, + Mailbox, ContextVar, Token, Field, Set, MutableSet,