typing ParamSpec (#5837)

* Add cspell

* TypeVar

* ParamSpec

* refactor

* TypeVarTuple repr

* Remove expectedFailure
This commit is contained in:
Jeong, YunWon
2025-06-26 20:31:17 +09:00
committed by GitHub
parent 1ae07813ee
commit 6905d4375b
5 changed files with 404 additions and 61 deletions

View File

@@ -17,6 +17,7 @@ basicsize
bdfl
bigcharset
bignum
bivariant
breakpointhook
cformat
chunksize

View File

@@ -742,8 +742,6 @@ class UnionTests(unittest.TestCase):
self.assertTrue(issubclass(dict, x))
self.assertFalse(issubclass(list, x))
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_instancecheck_and_subclasscheck_order(self):
T = typing.TypeVar('T')
@@ -790,8 +788,6 @@ class UnionTests(unittest.TestCase):
self.assertTrue(issubclass(int, x))
self.assertRaises(ZeroDivisionError, issubclass, list, x)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_or_type_operator_with_TypeVar(self):
TV = typing.TypeVar('T')
assert TV | str == typing.Union[TV, str]
@@ -799,8 +795,6 @@ class UnionTests(unittest.TestCase):
self.assertIs((int | TV)[int], int)
self.assertIs((TV | int)[int], int)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_union_args(self):
def check(arg, expected):
clear_typing_caches()
@@ -893,16 +887,12 @@ class UnionTests(unittest.TestCase):
self.assertEqual(copied.__args__, orig.__args__)
self.assertEqual(copied.__parameters__, orig.__parameters__)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_union_parameter_substitution_errors(self):
T = typing.TypeVar("T")
x = int | T
with self.assertRaises(TypeError):
x[int, str]
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_or_type_operator_with_forward(self):
T = typing.TypeVar('T')
ForwardAfter = T | 'Forward'

View File

@@ -464,8 +464,6 @@ class TypeVarTests(BaseTestCase):
self.assertEqual(Union[X, int].__parameters__, (X,))
self.assertIs(Union[X, int].__origin__, Union)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_or(self):
X = TypeVar('X')
# use a string because str doesn't implement
@@ -657,7 +655,6 @@ class TypeParameterDefaultsTests(BaseTestCase):
self.assertFalse(T.has_default())
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_paramspec(self):
P = ParamSpec('P', default=(str, int))
self.assertEqual(P.__default__, (str, int))
@@ -686,7 +683,6 @@ class TypeParameterDefaultsTests(BaseTestCase):
self.assertFalse(P.has_default())
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_typevartuple(self):
Ts = TypeVarTuple('Ts', default=Unpack[Tuple[str, int]])
self.assertEqual(Ts.__default__, Unpack[Tuple[str, int]])
@@ -1289,7 +1285,6 @@ class UnpackTests(BaseTestCase):
class TypeVarTupleTests(BaseTestCase):
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_name(self):
Ts = TypeVarTuple('Ts')
self.assertEqual(Ts.__name__, 'Ts')
@@ -1297,7 +1292,6 @@ class TypeVarTupleTests(BaseTestCase):
self.assertEqual(Ts2.__name__, 'Ts2')
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_module(self):
Ts = TypeVarTuple('Ts')
self.assertEqual(Ts.__module__, __name__)
@@ -2050,7 +2044,6 @@ class TypeVarTuplePicklingTests(BaseTestCase):
# statements at the start of each test.
# TODO: RUSTPYTHON
@unittest.expectedFailure
@all_pickle_protocols
def test_pickling_then_unpickling_results_in_same_identity(self, proto):
global global_Ts1 # See explanation at start of class.
@@ -4278,7 +4271,6 @@ class GenericTests(BaseTestCase):
self.assertEqual(t, deepcopy(t))
# TODO: RUSTPYTHON
@unittest.expectedFailure
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.
@@ -8801,7 +8793,6 @@ class TypeAliasTests(BaseTestCase):
class ParamSpecTests(BaseTestCase):
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_basic_plain(self):
P = ParamSpec('P')
self.assertEqual(P, P)
@@ -9010,7 +9001,6 @@ class ParamSpecTests(BaseTestCase):
self.assertEqual(B.__args__, ((int, str,), Tuple[bytes, float]))
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_var_substitution(self):
P = ParamSpec("P")
subst = P.__typing_subst__
@@ -9022,7 +9012,6 @@ class ParamSpecTests(BaseTestCase):
self.assertEqual(subst(Concatenate[int, P]), Concatenate[int, P])
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_bad_var_substitution(self):
T = TypeVar('T')
P = ParamSpec('P')
@@ -9241,7 +9230,6 @@ class ConcatenateTests(BaseTestCase):
self.assertIn(required_item, dir_items)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_valid_uses(self):
P = ParamSpec('P')
T = TypeVar('T')
@@ -9719,8 +9707,6 @@ class NoDefaultTests(BaseTestCase):
with self.assertRaises(TypeError):
type(NoDefault)(1)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_repr(self):
self.assertEqual(repr(NoDefault), 'typing.NoDefault')

View File

@@ -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())
_typing::make_typevartuple(type_var_tuple_name.clone(), vm)
.into_ref(&vm.ctx)
.into();
self.push_value(type_var_tuple);

View File

@@ -16,7 +16,8 @@ pub(crate) mod _typing {
AsObject, PyObjectRef, PyPayload, PyResult, VirtualMachine,
builtins::{PyGenericAlias, PyTupleRef, PyTypeRef, pystr::AsPyStr},
function::{FuncArgs, IntoFuncArgs},
types::{Constructor, Representable},
protocol::PyNumberMethods,
types::{AsNumber, Constructor, Representable},
};
pub(crate) fn _call_typing_func_object<'a>(
@@ -59,7 +60,7 @@ pub(crate) mod _typing {
contravariant: bool,
infer_variance: bool,
}
#[pyclass(flags(HAS_DICT), with(Constructor, Representable))]
#[pyclass(flags(HAS_DICT), with(AsNumber, Constructor, Representable))]
impl TypeVar {
#[pymethod(magic)]
fn mro_entries(&self, _bases: PyObjectRef, vm: &VirtualMachine) -> PyResult {
@@ -173,6 +174,18 @@ pub(crate) mod _typing {
}
}
impl AsNumber for TypeVar {
fn as_number() -> &'static PyNumberMethods {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| {
_call_typing_func_object(vm, "_make_union", (a.to_owned(), b.to_owned()))
}),
..PyNumberMethods::NOT_IMPLEMENTED
};
&AS_NUMBER
}
}
impl Constructor for TypeVar {
type Args = FuncArgs;
@@ -283,17 +296,7 @@ pub(crate) mod _typing {
let obj = typevar.into_ref_with_type(vm, cls)?;
let obj_ref: PyObjectRef = obj.into();
// Set __module__ from the current frame
if let Some(frame) = vm.current_frame() {
if let Ok(module_name) = frame.globals.get_item("__name__", vm) {
// Don't set __module__ if it's None
if !vm.is_none(&module_name) {
obj_ref.set_attr("__module__", module_name, vm)?;
}
}
}
set_module_from_caller(&obj_ref, vm)?;
Ok(obj_ref)
}
}
@@ -319,7 +322,7 @@ pub(crate) mod _typing {
}
#[pyattr]
#[pyclass(name = "ParamSpec")]
#[pyclass(name = "ParamSpec", module = "typing")]
#[derive(Debug, PyPayload)]
#[allow(dead_code)]
pub(crate) struct ParamSpec {
@@ -332,13 +335,31 @@ pub(crate) mod _typing {
infer_variance: bool,
}
#[pyclass(flags(BASETYPE))]
#[pyclass(flags(HAS_DICT), with(AsNumber, Constructor))]
impl ParamSpec {
#[pygetset(magic)]
fn name(&self) -> PyObjectRef {
self.name.clone()
}
#[pygetset]
fn args(zelf: crate::PyRef<Self>, vm: &VirtualMachine) -> PyResult {
let self_obj: PyObjectRef = zelf.into();
let psa = ParamSpecArgs {
__origin__: self_obj,
};
Ok(psa.into_ref(&vm.ctx).into())
}
#[pygetset]
fn kwargs(zelf: crate::PyRef<Self>, vm: &VirtualMachine) -> PyResult {
let self_obj: PyObjectRef = zelf.into();
let psk = ParamSpecKwargs {
__origin__: self_obj,
};
Ok(psk.into_ref(&vm.ctx).into())
}
#[pygetset(magic)]
fn bound(&self, vm: &VirtualMachine) -> PyObjectRef {
if let Some(bound) = self.bound.clone() {
@@ -364,16 +385,19 @@ pub(crate) mod _typing {
#[pygetset(magic)]
fn default(&self, vm: &VirtualMachine) -> PyResult {
if let Some(default_value) = self.default_value.clone() {
return Ok(default_value);
if let Some(ref default_value) = self.default_value {
// Check if default_value is NoDefault (not just None)
if !default_value.is(&vm.ctx.typing_no_default) {
return Ok(default_value.clone());
}
}
// handle evaluate_default
if let Some(evaluate_default) = self.evaluate_default.clone() {
let default_value = vm.call_method(evaluate_default.as_ref(), "__call__", ())?;
let default_value = evaluate_default.call((), vm)?;
return Ok(default_value);
}
// TODO: this isn't up to spec
Ok(vm.ctx.none())
// Return NoDefault singleton
Ok(vm.ctx.typing_no_default.clone().into())
}
#[pygetset]
@@ -381,7 +405,6 @@ pub(crate) mod _typing {
if let Some(evaluate_default) = self.evaluate_default.clone() {
return evaluate_default;
}
// TODO: default_value case
vm.ctx.none()
}
@@ -391,9 +414,137 @@ pub(crate) mod _typing {
}
#[pymethod]
fn has_default(&self) -> PyResult<bool> {
// TODO: fix
Ok(self.evaluate_default.is_some() || self.default_value.is_some())
fn has_default(&self, vm: &VirtualMachine) -> bool {
if self.evaluate_default.is_some() {
return true;
}
if let Some(ref default_value) = self.default_value {
// Check if default_value is not NoDefault
!default_value.is(&vm.ctx.typing_no_default)
} else {
false
}
}
#[pymethod(magic)]
fn typing_subst(
zelf: crate::PyRef<Self>,
arg: PyObjectRef,
vm: &VirtualMachine,
) -> PyResult {
let self_obj: PyObjectRef = zelf.into();
_call_typing_func_object(vm, "_paramspec_subst", (self_obj, arg))
}
#[pymethod(magic)]
fn typing_prepare_subst(
zelf: crate::PyRef<Self>,
alias: PyObjectRef,
args: PyTupleRef,
vm: &VirtualMachine,
) -> PyResult {
let self_obj: PyObjectRef = zelf.into();
_call_typing_func_object(vm, "_paramspec_prepare_subst", (self_obj, alias, args))
}
}
impl AsNumber for ParamSpec {
fn as_number() -> &'static PyNumberMethods {
static AS_NUMBER: PyNumberMethods = PyNumberMethods {
or: Some(|a, b, vm| {
_call_typing_func_object(vm, "_make_union", (a.to_owned(), b.to_owned()))
}),
..PyNumberMethods::NOT_IMPLEMENTED
};
&AS_NUMBER
}
}
impl Constructor for ParamSpec {
type Args = FuncArgs;
fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult {
let mut kwargs = args.kwargs;
// Parse arguments manually
let name = if args.args.is_empty() {
// Check if name is provided as keyword argument
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(),
));
}
} 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())
);
};
let bound = kwargs.swap_remove("bound");
let covariant = kwargs
.swap_remove("covariant")
.map(|v| v.try_to_bool(vm))
.transpose()?
.unwrap_or(false);
let contravariant = kwargs
.swap_remove("contravariant")
.map(|v| v.try_to_bool(vm))
.transpose()?
.unwrap_or(false);
let infer_variance = kwargs
.swap_remove("infer_variance")
.map(|v| v.try_to_bool(vm))
.transpose()?
.unwrap_or(false);
let default = kwargs.swap_remove("default");
// Check for unexpected keyword arguments
if !kwargs.is_empty() {
let unexpected_keys: Vec<String> = kwargs.keys().map(|s| s.to_string()).collect();
return Err(vm.new_type_error(format!(
"ParamSpec() got unexpected keyword argument(s): {}",
unexpected_keys.join(", ")
)));
}
// Check for invalid combinations
if covariant && contravariant {
return Err(
vm.new_value_error("Bivariant type variables are not supported.".to_owned())
);
}
if infer_variance && (covariant || contravariant) {
return Err(vm.new_value_error(
"Variance cannot be specified with infer_variance".to_owned(),
));
}
// Handle default value
let default_value = if let Some(default) = default {
Some(default)
} else {
// If no default provided, use NoDefault singleton
Some(vm.ctx.typing_no_default.clone().into())
};
let paramspec = ParamSpec {
name,
bound,
default_value,
evaluate_default: None,
covariant,
contravariant,
infer_variance,
};
let obj = paramspec.into_ref_with_type(vm, cls)?;
let obj_ref: PyObjectRef = obj.into();
set_module_from_caller(&obj_ref, vm)?;
Ok(obj_ref)
}
}
@@ -413,7 +564,7 @@ pub(crate) mod _typing {
#[derive(Debug, PyPayload)]
pub struct NoDefault;
#[pyclass(with(Constructor), flags(BASETYPE))]
#[pyclass(with(Constructor, Representable), flags(BASETYPE))]
impl NoDefault {
#[pymethod(magic)]
fn reduce(&self, _vm: &VirtualMachine) -> String {
@@ -442,34 +593,213 @@ pub(crate) mod _typing {
}
#[pyattr]
#[pyclass(name = "TypeVarTuple")]
#[pyclass(name = "TypeVarTuple", module = "typing")]
#[derive(Debug, PyPayload)]
#[allow(dead_code)]
pub(crate) struct TypeVarTuple {
name: PyObjectRef,
default_value: parking_lot::Mutex<PyObjectRef>,
evaluate_default: PyObjectRef,
}
#[pyclass(flags(BASETYPE))]
impl TypeVarTuple {}
#[pyclass(flags(HAS_DICT), with(Constructor, Representable))]
impl TypeVarTuple {
#[pygetset(magic)]
fn name(&self) -> PyObjectRef {
self.name.clone()
}
pub(crate) fn make_typevartuple(name: PyObjectRef) -> TypeVarTuple {
TypeVarTuple { name }
#[pygetset(magic)]
fn default(&self, vm: &VirtualMachine) -> PyResult {
let mut default_value = self.default_value.lock();
// Check if default_value is NoDefault (not just None)
if !default_value.is(&vm.ctx.typing_no_default) {
return Ok(default_value.clone());
}
if !vm.is_none(&self.evaluate_default) {
*default_value = self.evaluate_default.call((), vm)?;
Ok(default_value.clone())
} else {
// Return NoDefault singleton
Ok(vm.ctx.typing_no_default.clone().into())
}
}
#[pymethod]
fn has_default(&self, vm: &VirtualMachine) -> bool {
if !vm.is_none(&self.evaluate_default) {
return true;
}
let default_value = self.default_value.lock();
// Check if default_value is not NoDefault
!default_value.is(&vm.ctx.typing_no_default)
}
#[pymethod(magic)]
fn reduce(&self) -> PyObjectRef {
self.name.clone()
}
}
impl Constructor for TypeVarTuple {
type Args = FuncArgs;
fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult {
let mut kwargs = args.kwargs;
// Parse arguments manually
let name = if args.args.is_empty() {
// Check if name is provided as keyword argument
if let Some(name) = kwargs.swap_remove("name") {
name
} else {
return Err(vm.new_type_error(
"TypeVarTuple() missing required argument: 'name' (pos 1)".to_owned(),
));
}
} 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(),
));
};
let default = kwargs.swap_remove("default");
// Check for unexpected keyword arguments
if !kwargs.is_empty() {
let unexpected_keys: Vec<String> = kwargs.keys().map(|s| s.to_string()).collect();
return Err(vm.new_type_error(format!(
"TypeVarTuple() got unexpected keyword argument(s): {}",
unexpected_keys.join(", ")
)));
}
// Handle default value
let (default_value, evaluate_default) = if let Some(default) = default {
(default, vm.ctx.none())
} else {
// If no default provided, use NoDefault singleton
(vm.ctx.typing_no_default.clone().into(), vm.ctx.none())
};
let typevartuple = TypeVarTuple {
name,
default_value: parking_lot::Mutex::new(default_value),
evaluate_default,
};
let obj = typevartuple.into_ref_with_type(vm, cls)?;
let obj_ref: PyObjectRef = obj.into();
set_module_from_caller(&obj_ref, vm)?;
Ok(obj_ref)
}
}
impl Representable for TypeVarTuple {
#[inline(always)]
fn repr_str(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
let name = zelf.name.str(vm)?;
Ok(format!("*{}", name))
}
}
pub(crate) fn make_typevartuple(name: PyObjectRef, vm: &VirtualMachine) -> TypeVarTuple {
TypeVarTuple {
name,
default_value: parking_lot::Mutex::new(vm.ctx.typing_no_default.clone().into()),
evaluate_default: vm.ctx.none(),
}
}
#[pyattr]
#[pyclass(name = "ParamSpecArgs")]
#[derive(Debug, PyPayload)]
#[allow(dead_code)]
pub(crate) struct ParamSpecArgs {}
#[pyclass(flags(BASETYPE))]
impl ParamSpecArgs {}
pub(crate) struct ParamSpecArgs {
__origin__: PyObjectRef,
}
#[pyclass(flags(BASETYPE), with(Constructor, Representable))]
impl ParamSpecArgs {
#[pygetset(magic)]
fn origin(&self) -> PyObjectRef {
self.__origin__.clone()
}
#[pymethod(magic)]
fn eq(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
// 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 {
type Args = (PyObjectRef,);
fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult {
let origin = args.0;
let psa = ParamSpecArgs { __origin__: origin };
psa.into_ref_with_type(vm, cls).map(Into::into)
}
}
impl Representable for ParamSpecArgs {
#[inline(always)]
fn repr_str(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
// Check if origin is a ParamSpec
if let Ok(name) = zelf.__origin__.get_attr("__name__", vm) {
return Ok(format!("{}.args", name.str(vm)?));
}
Ok(format!("{:?}.args", zelf.__origin__))
}
}
#[pyattr]
#[pyclass(name = "ParamSpecKwargs")]
#[derive(Debug, PyPayload)]
#[allow(dead_code)]
pub(crate) struct ParamSpecKwargs {}
#[pyclass(flags(BASETYPE))]
impl ParamSpecKwargs {}
pub(crate) struct ParamSpecKwargs {
__origin__: PyObjectRef,
}
#[pyclass(flags(BASETYPE), with(Constructor, Representable))]
impl ParamSpecKwargs {
#[pygetset(magic)]
fn origin(&self) -> PyObjectRef {
self.__origin__.clone()
}
#[pymethod(magic)]
fn eq(&self, other: PyObjectRef, vm: &VirtualMachine) -> PyResult<bool> {
// 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 {
type Args = (PyObjectRef,);
fn py_new(cls: PyTypeRef, args: Self::Args, vm: &VirtualMachine) -> PyResult {
let origin = args.0;
let psa = ParamSpecKwargs { __origin__: origin };
psa.into_ref_with_type(vm, cls).map(Into::into)
}
}
impl Representable for ParamSpecKwargs {
#[inline(always)]
fn repr_str(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
// Check if origin is a ParamSpec
if let Ok(name) = zelf.__origin__.get_attr("__name__", vm) {
return Ok(format!("{}.kwargs", name.str(vm)?));
}
Ok(format!("{:?}.kwargs", zelf.__origin__))
}
}
#[pyattr]
#[pyclass(name)]
@@ -523,4 +853,40 @@ pub(crate) mod _typing {
// &AS_MAPPING
// }
// }
/// Get the module of the caller frame, similar to CPython's caller() function.
/// Returns the module name or None if not found.
///
/// Note: CPython's implementation (in typevarobject.c) gets the module from the
/// frame's function object using PyFunction_GetModule(f->f_funcobj). However,
/// RustPython's Frame doesn't store a reference to the function object, so we
/// get the module name from the frame's globals dictionary instead.
fn caller(vm: &VirtualMachine) -> Option<PyObjectRef> {
let frame = vm.current_frame()?;
// In RustPython, we get the module name from frame's globals
// This is similar to CPython's sys._getframe().f_globals.get('__name__')
frame.globals.get_item("__name__", vm).ok()
}
/// Set __module__ attribute for an object based on the caller's module.
/// This follows CPython's behavior for TypeVar and similar objects.
fn set_module_from_caller(obj: &PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
// Note: CPython gets module from frame->f_funcobj, but RustPython's Frame
// architecture is different - we use globals['__name__'] instead
if let Some(module_name) = caller(vm) {
// Special handling for certain module names
if let Ok(name_str) = module_name.str(vm) {
let name = name_str.as_str();
// CPython sets __module__ to None for builtins and <...> modules
if name == "builtins" || name.starts_with('<') {
// Don't set __module__ attribute at all (CPython behavior)
// This allows the typing module to handle it
return Ok(());
}
}
obj.set_attr("__module__", module_name, vm)?;
}
Ok(())
}
}