diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2ce20c75ae..f2427688c8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -183,6 +183,27 @@ cargo run --features jit cargo run --features ssl ``` +## Test Code Modification Rules + +**CRITICAL: Test code modification restrictions** +- NEVER comment out or delete any test code lines except for removing `@unittest.expectedFailure` decorators and upper TODO comments +- NEVER modify test assertions, test logic, or test data +- When a test cannot pass due to missing language features, keep it as expectedFailure and document the reason +- The only acceptable modifications to test files are: + 1. Removing `@unittest.expectedFailure` decorators and the upper TODO comments when tests actually pass + 2. Adding `@unittest.expectedFailure` decorators when tests cannot be fixed + +**Examples of FORBIDDEN modifications:** +- Commenting out test lines +- Changing test assertions +- Modifying test data or expected results +- Removing test logic + +**Correct approach when tests fail due to unsupported syntax:** +- Keep the test as `@unittest.expectedFailure` +- Document that it requires PEP 695 support +- Focus on tests that can be fixed through Rust code changes only + ## Documentation - Check the [architecture document](architecture/architecture.md) for a high-level overview diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 41f4e72115..c13b154ac8 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -373,7 +373,6 @@ def test_alias(self): self.assertEqual(get_args(alias_3), (LiteralString,)) class TypeVarTests(BaseTestCase): - # TODO: RUSTPYTHON def test_basic_plain(self): T = TypeVar('T') # T equals itself. @@ -388,8 +387,6 @@ def test_basic_plain(self): self.assertIs(T.__infer_variance__, False) self.assertEqual(T.__module__, __name__) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_basic_with_exec(self): ns = {} exec('from typing import TypeVar; T = TypeVar("T", bound=float)', ns, ns) @@ -403,7 +400,6 @@ def test_basic_with_exec(self): self.assertIs(T.__infer_variance__, False) self.assertIs(T.__module__, None) - # TODO: RUSTPYTHON def test_attributes(self): T_bound = TypeVar('T_bound', bound=int) self.assertEqual(T_bound.__name__, 'T_bound') @@ -445,7 +441,6 @@ def test_typevar_subclass_type_error(self): with self.assertRaises(TypeError): issubclass(T, int) - # TODO: RUSTPYTHON def test_constrained_error(self): with self.assertRaises(TypeError): X = TypeVar('X', int) @@ -478,7 +473,6 @@ def test_union_constrained(self): A = TypeVar('A', str, bytes) self.assertNotEqual(Union[A, str], Union[A]) - # TODO: RUSTPYTHON def test_repr(self): self.assertEqual(repr(T), '~T') self.assertEqual(repr(KT), '~KT') @@ -493,7 +487,6 @@ def test_no_redefinition(self): self.assertNotEqual(TypeVar('T'), TypeVar('T')) self.assertNotEqual(TypeVar('T', int, str), TypeVar('T', int, str)) - # TODO: RUSTPYTHON def test_cannot_subclass(self): with self.assertRaisesRegex(TypeError, NOT_A_BASE_TYPE % 'TypeVar'): class V(TypeVar): pass @@ -573,7 +566,6 @@ def test_many_weakrefs(self): vals[x] = cls(str(x)) del vals - # TODO: RUSTPYTHON def test_constructor(self): T = TypeVar(name="T") self.assertEqual(T.__name__, "T") @@ -654,7 +646,6 @@ class X[T]: ... self.assertIs(T.__default__, NoDefault) self.assertFalse(T.has_default()) - # TODO: RUSTPYTHON def test_paramspec(self): P = ParamSpec('P', default=(str, int)) self.assertEqual(P.__default__, (str, int)) @@ -682,7 +673,6 @@ class X[**P]: ... self.assertIs(P.__default__, NoDefault) self.assertFalse(P.has_default()) - # TODO: RUSTPYTHON def test_typevartuple(self): Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]]) self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]]) @@ -1284,20 +1274,16 @@ class Gen[*Ts]: ... class TypeVarTupleTests(BaseTestCase): - # TODO: RUSTPYTHON def test_name(self): Ts = TypeVarTuple('Ts') self.assertEqual(Ts.__name__, 'Ts') Ts2 = TypeVarTuple('Ts2') self.assertEqual(Ts2.__name__, 'Ts2') - # TODO: RUSTPYTHON def test_module(self): Ts = TypeVarTuple('Ts') self.assertEqual(Ts.__module__, __name__) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_exec(self): ns = {} exec('from typing import TypeVarTuple; Ts = TypeVarTuple("Ts")', ns) @@ -4270,7 +4256,6 @@ class Node(Generic[T]): ... self.assertEqual(t, copy(t)) self.assertEqual(t, deepcopy(t)) - # TODO: RUSTPYTHON def test_immutability_by_copy_and_pickle(self): # Special forms like Union, Any, etc., generic aliases to containers like List, # Mapping, etc., and type variabcles are considered immutable by copy and pickle. @@ -8792,7 +8777,6 @@ def test_cannot_subscript(self): class ParamSpecTests(BaseTestCase): - # TODO: RUSTPYTHON def test_basic_plain(self): P = ParamSpec('P') self.assertEqual(P, P) @@ -8800,8 +8784,6 @@ def test_basic_plain(self): self.assertEqual(P.__name__, 'P') self.assertEqual(P.__module__, __name__) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_basic_with_exec(self): ns = {} exec('from typing import ParamSpec; P = ParamSpec("P")', ns, ns) @@ -9000,7 +8982,6 @@ class Y(Generic[P, T]): B = A[[int, str], bytes, float] self.assertEqual(B.__args__, ((int, str,), Tuple[bytes, float])) - # TODO: RUSTPYTHON def test_var_substitution(self): P = ParamSpec("P") subst = P.__typing_subst__ @@ -9011,7 +8992,6 @@ def test_var_substitution(self): self.assertIs(subst(P), P) self.assertEqual(subst(Concatenate[int, P]), Concatenate[int, P]) - # TODO: RUSTPYTHON def test_bad_var_substitution(self): T = TypeVar('T') P = ParamSpec('P') @@ -9191,8 +9171,6 @@ def test_paramspec_gets_copied(self): self.assertEqual(C2[Concatenate[str, P2]].__parameters__, (P2,)) self.assertEqual(C2[Concatenate[T, P2]].__parameters__, (T, P2)) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_cannot_subclass(self): with self.assertRaisesRegex(TypeError, NOT_A_BASE_TYPE % 'ParamSpec'): class C(ParamSpec): pass @@ -9229,7 +9207,6 @@ def test_dir(self): with self.subTest(required_item=required_item): self.assertIn(required_item, dir_items) - # TODO: RUSTPYTHON def test_valid_uses(self): P = ParamSpec('P') T = TypeVar('T') diff --git a/vm/src/frame.rs b/vm/src/frame.rs index abb90a7e40..16ab4db266 100644 --- a/vm/src/frame.rs +++ b/vm/src/frame.rs @@ -17,7 +17,7 @@ use crate::{ protocol::{PyIter, PyIterReturn}, scope::Scope, source::SourceLocation, - stdlib::{builtins, typing::_typing}, + stdlib::{builtins, typing}, vm::{Context, PyMethod}, }; use indexmap::IndexMap; @@ -1234,7 +1234,7 @@ impl ExecutingFrame<'_> { bytecode::Instruction::TypeVar => { let type_name = self.pop_value(); let type_var: PyObjectRef = - _typing::make_typevar(vm, type_name.clone(), vm.ctx.none(), vm.ctx.none()) + typing::make_typevar(vm, type_name.clone(), vm.ctx.none(), vm.ctx.none()) .into_ref(&vm.ctx) .into(); self.push_value(type_var); @@ -1244,7 +1244,7 @@ impl ExecutingFrame<'_> { let type_name = self.pop_value(); let bound = self.pop_value(); let type_var: PyObjectRef = - _typing::make_typevar(vm, type_name.clone(), bound, vm.ctx.none()) + typing::make_typevar(vm, type_name.clone(), bound, vm.ctx.none()) .into_ref(&vm.ctx) .into(); self.push_value(type_var); @@ -1254,7 +1254,7 @@ impl ExecutingFrame<'_> { let type_name = self.pop_value(); let constraint = self.pop_value(); let type_var: PyObjectRef = - _typing::make_typevar(vm, type_name.clone(), vm.ctx.none(), constraint) + typing::make_typevar(vm, type_name.clone(), vm.ctx.none(), constraint) .into_ref(&vm.ctx) .into(); self.push_value(type_var); @@ -1267,13 +1267,13 @@ impl ExecutingFrame<'_> { .downcast() .map_err(|_| vm.new_type_error("Type params must be a tuple."))?; let value = self.pop_value(); - let type_alias = _typing::TypeAliasType::new(name, type_params, value); + let type_alias = typing::TypeAliasType::new(name, type_params, value); self.push_value(type_alias.into_ref(&vm.ctx).into()); Ok(None) } bytecode::Instruction::ParamSpec => { let param_spec_name = self.pop_value(); - let param_spec: PyObjectRef = _typing::make_paramspec(param_spec_name.clone()) + let param_spec: PyObjectRef = typing::make_paramspec(param_spec_name.clone()) .into_ref(&vm.ctx) .into(); self.push_value(param_spec); @@ -1282,7 +1282,7 @@ impl ExecutingFrame<'_> { bytecode::Instruction::TypeVarTuple => { let type_var_tuple_name = self.pop_value(); let type_var_tuple: PyObjectRef = - _typing::make_typevartuple(type_var_tuple_name.clone(), vm) + typing::make_typevartuple(type_var_tuple_name.clone(), vm) .into_ref(&vm.ctx) .into(); self.push_value(type_var_tuple); diff --git a/vm/src/stdlib/typing.rs b/vm/src/stdlib/typing.rs index 1871392cb3..33427387f7 100644 --- a/vm/src/stdlib/typing.rs +++ b/vm/src/stdlib/typing.rs @@ -1,23 +1,23 @@ use crate::{PyRef, VirtualMachine, stdlib::PyModule}; -pub(crate) use _typing::NoDefault; +pub(crate) use decl::*; pub(crate) fn make_module(vm: &VirtualMachine) -> PyRef { - let module = _typing::make_module(vm); + let module = decl::make_module(vm); extend_module!(vm, &module, { "NoDefault" => vm.ctx.typing_no_default.clone(), }); module } -#[pymodule] -pub(crate) mod _typing { +#[pymodule(name = "_typing")] +pub(crate) mod decl { use crate::{ - AsObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, + AsObject, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, builtins::{PyGenericAlias, PyTupleRef, PyTypeRef, pystr::AsPyStr}, - function::{FuncArgs, IntoFuncArgs}, + function::{FuncArgs, IntoFuncArgs, PyComparisonValue}, protocol::PyNumberMethods, - types::{AsNumber, Constructor, Representable}, + types::{AsNumber, Comparable, Constructor, PyComparisonOp, Representable}, }; pub(crate) fn _call_typing_func_object<'a>( @@ -44,6 +44,13 @@ pub(crate) mod _typing { args.args[0].clone() } + #[pyfunction(name = "override")] + pub(crate) fn r#override(func: PyObjectRef, vm: &VirtualMachine) -> PyResult { + // Set __override__ attribute to True + func.set_attr("__override__", vm.ctx.true_value.clone(), vm)?; + Ok(func) + } + #[pyattr] #[pyclass(name = "TypeVar", module = "typing")] #[derive(Debug, PyPayload)] @@ -337,6 +344,11 @@ pub(crate) mod _typing { #[pyclass(flags(HAS_DICT), with(AsNumber, Constructor))] impl ParamSpec { + #[pymethod(magic)] + fn mro_entries(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("Cannot subclass an instance of ParamSpec")) + } + #[pygetset(magic)] fn name(&self) -> PyObjectRef { self.name.clone() @@ -471,16 +483,14 @@ pub(crate) mod _typing { if let Some(name) = kwargs.swap_remove("name") { name } else { - return Err(vm.new_type_error( - "ParamSpec() missing required argument: 'name' (pos 1)".to_owned(), - )); + return Err( + vm.new_type_error("ParamSpec() missing required argument: 'name' (pos 1)") + ); } } else if args.args.len() == 1 { args.args[0].clone() } else { - return Err( - vm.new_type_error("ParamSpec() takes at most 1 positional argument".to_owned()) - ); + return Err(vm.new_type_error("ParamSpec() takes at most 1 positional argument")); }; let bound = kwargs.swap_remove("bound"); @@ -512,15 +522,11 @@ pub(crate) mod _typing { // Check for invalid combinations if covariant && contravariant { - return Err( - vm.new_value_error("Bivariant type variables are not supported.".to_owned()) - ); + return Err(vm.new_value_error("Bivariant type variables are not supported.")); } if infer_variance && (covariant || contravariant) { - return Err(vm.new_value_error( - "Variance cannot be specified with infer_variance".to_owned(), - )); + return Err(vm.new_value_error("Variance cannot be specified with infer_variance")); } // Handle default value @@ -638,6 +644,27 @@ pub(crate) mod _typing { fn reduce(&self) -> PyObjectRef { self.name.clone() } + + #[pymethod(magic)] + fn mro_entries(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("Cannot subclass an instance of TypeVarTuple")) + } + + #[pymethod(magic)] + fn typing_subst(&self, _arg: PyObjectRef, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("Substitution of bare TypeVarTuple is not supported")) + } + + #[pymethod(magic)] + fn typing_prepare_subst( + zelf: crate::PyRef, + alias: PyObjectRef, + args: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + let self_obj: PyObjectRef = zelf.into(); + _call_typing_func_object(vm, "_typevartuple_prepare_subst", (self_obj, alias, args)) + } } impl Constructor for TypeVarTuple { @@ -652,15 +679,13 @@ pub(crate) mod _typing { name } else { return Err(vm.new_type_error( - "TypeVarTuple() missing required argument: 'name' (pos 1)".to_owned(), + "TypeVarTuple() missing required argument: 'name' (pos 1)", )); } } else if args.args.len() == 1 { args.args[0].clone() } else { - return Err(vm.new_type_error( - "TypeVarTuple() takes at most 1 positional argument".to_owned(), - )); + return Err(vm.new_type_error("TypeVarTuple() takes at most 1 positional argument")); }; let default = kwargs.swap_remove("default"); @@ -712,27 +737,23 @@ pub(crate) mod _typing { } #[pyattr] - #[pyclass(name = "ParamSpecArgs")] + #[pyclass(name = "ParamSpecArgs", module = "typing")] #[derive(Debug, PyPayload)] #[allow(dead_code)] pub(crate) struct ParamSpecArgs { __origin__: PyObjectRef, } - #[pyclass(flags(BASETYPE), with(Constructor, Representable))] + #[pyclass(with(Constructor, Representable, Comparable))] impl ParamSpecArgs { + #[pymethod(magic)] + fn mro_entries(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("Cannot subclass an instance of ParamSpecArgs")) + } + #[pygetset(magic)] fn origin(&self) -> PyObjectRef { self.__origin__.clone() } - - #[pymethod(magic)] - fn eq(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyResult { - // Check if other has __origin__ attribute - if let Ok(other_origin) = other.get_attr("__origin__", vm) { - return Ok(self.__origin__.is(&other_origin)); - } - Ok(false) - } } impl Constructor for ParamSpecArgs { @@ -756,28 +777,62 @@ pub(crate) mod _typing { } } + impl Comparable for ParamSpecArgs { + fn cmp( + zelf: &crate::Py, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult { + fn eq( + zelf: &crate::Py, + other: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + // Check if other has __origin__ attribute + if let Ok(other_origin) = other.get_attr("__origin__", vm) { + return Ok(zelf.__origin__.is(&other_origin)); + } + Ok(false) + } + match op { + PyComparisonOp::Eq => { + if let Ok(result) = eq(zelf, other.to_owned(), vm) { + Ok(result.into()) + } else { + Ok(PyComparisonValue::NotImplemented) + } + } + PyComparisonOp::Ne => { + if let Ok(result) = eq(zelf, other.to_owned(), vm) { + Ok((!result).into()) + } else { + Ok(PyComparisonValue::NotImplemented) + } + } + _ => Ok(PyComparisonValue::NotImplemented), + } + } + } + #[pyattr] - #[pyclass(name = "ParamSpecKwargs")] + #[pyclass(name = "ParamSpecKwargs", module = "typing")] #[derive(Debug, PyPayload)] #[allow(dead_code)] pub(crate) struct ParamSpecKwargs { __origin__: PyObjectRef, } - #[pyclass(flags(BASETYPE), with(Constructor, Representable))] + #[pyclass(with(Constructor, Representable, Comparable))] impl ParamSpecKwargs { + #[pymethod(magic)] + fn mro_entries(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult { + Err(vm.new_type_error("Cannot subclass an instance of ParamSpecKwargs")) + } + #[pygetset(magic)] fn origin(&self) -> PyObjectRef { self.__origin__.clone() } - - #[pymethod(magic)] - fn eq(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyResult { - // Check if other has __origin__ attribute - if let Ok(other_origin) = other.get_attr("__origin__", vm) { - return Ok(self.__origin__.is(&other_origin)); - } - Ok(false) - } } impl Constructor for ParamSpecKwargs { @@ -801,6 +856,44 @@ pub(crate) mod _typing { } } + impl Comparable for ParamSpecKwargs { + fn cmp( + zelf: &crate::Py, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult { + fn eq( + zelf: &crate::Py, + other: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult { + // Check if other has __origin__ attribute + if let Ok(other_origin) = other.get_attr("__origin__", vm) { + return Ok(zelf.__origin__.is(&other_origin)); + } + Ok(false) + } + match op { + PyComparisonOp::Eq => { + if let Ok(result) = eq(zelf, other.to_owned(), vm) { + Ok(result.into()) + } else { + Ok(PyComparisonValue::NotImplemented) + } + } + PyComparisonOp::Ne => { + if let Ok(result) = eq(zelf, other.to_owned(), vm) { + Ok((!result).into()) + } else { + Ok(PyComparisonValue::NotImplemented) + } + } + _ => Ok(PyComparisonValue::NotImplemented), + } + } + } + #[pyattr] #[pyclass(name)] #[derive(Debug, PyPayload)] @@ -879,6 +972,7 @@ pub(crate) mod _typing { if let Ok(name_str) = module_name.str(vm) { let name = name_str.as_str(); // CPython sets __module__ to None for builtins and <...> modules + // Also set to None for exec contexts (no __name__ in globals means exec) if name == "builtins" || name.starts_with('<') { // Don't set __module__ attribute at all (CPython behavior) // This allows the typing module to handle it @@ -886,6 +980,9 @@ pub(crate) mod _typing { } } obj.set_attr("__module__", module_name, vm)?; + } else { + // If no module name is found (e.g., in exec context), set __module__ to None + obj.set_attr("__module__", vm.ctx.none(), vm)?; } Ok(()) }