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, 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/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}; 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(()) }