diff --git a/crates/red_knot_python_semantic/resources/mdtest/generics.md b/crates/red_knot_python_semantic/resources/mdtest/generics.md index cc1eb392a9..b65f2c7c94 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/generics.md +++ b/crates/red_knot_python_semantic/resources/mdtest/generics.md @@ -60,52 +60,20 @@ reveal_type(S) # revealed: Literal[S] ## Type params -A PEP695 type variable defines a value of type `typing.TypeVar` with attributes `__name__`, -`__bounds__`, `__constraints__`, and `__default__` (the latter three all lazily evaluated): +A PEP695 type variable defines a value of type `typing.TypeVar`. ```py -def f[T, U: A, V: (A, B), W = A, X: A = A1](): +def f[T](): reveal_type(T) # revealed: T reveal_type(T.__name__) # revealed: Literal["T"] - reveal_type(T.__bound__) # revealed: None - reveal_type(T.__constraints__) # revealed: tuple[()] - reveal_type(T.__default__) # revealed: NoDefault - - reveal_type(U) # revealed: U - reveal_type(U.__name__) # revealed: Literal["U"] - reveal_type(U.__bound__) # revealed: type[A] - reveal_type(U.__constraints__) # revealed: tuple[()] - reveal_type(U.__default__) # revealed: NoDefault - - reveal_type(V) # revealed: V - reveal_type(V.__name__) # revealed: Literal["V"] - reveal_type(V.__bound__) # revealed: None - reveal_type(V.__constraints__) # revealed: tuple[type[A], type[B]] - reveal_type(V.__default__) # revealed: NoDefault - - reveal_type(W) # revealed: W - reveal_type(W.__name__) # revealed: Literal["W"] - reveal_type(W.__bound__) # revealed: None - reveal_type(W.__constraints__) # revealed: tuple[()] - reveal_type(W.__default__) # revealed: type[A] - - reveal_type(X) # revealed: X - reveal_type(X.__name__) # revealed: Literal["X"] - reveal_type(X.__bound__) # revealed: type[A] - reveal_type(X.__constraints__) # revealed: tuple[()] - reveal_type(X.__default__) # revealed: type[A1] - -class A: ... -class B: ... -class A1(A): ... ``` ## Minimum two constraints -A typevar with less than two constraints emits a diagnostic and is treated as unconstrained: +A typevar with less than two constraints emits a diagnostic: ```py # error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types" def f[T: (int,)](): - reveal_type(T.__constraints__) # revealed: tuple[()] + pass ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 89aa8077a8..5edc68a469 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -537,6 +537,19 @@ impl<'db> Type<'db> { .expect("Expected a Type::IntLiteral variant") } + pub const fn into_known_instance(self) -> Option> { + match self { + Type::KnownInstance(known_instance) => Some(known_instance), + _ => None, + } + } + + #[track_caller] + pub fn expect_known_instance(self) -> KnownInstanceType<'db> { + self.into_known_instance() + .expect("Expected a Type::KnownInstance variant") + } + pub const fn is_boolean_literal(&self) -> bool { matches!(self, Type::BooleanLiteral(..)) } @@ -1951,23 +1964,6 @@ impl<'db> KnownInstanceType<'db> { fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> { let ty = match (self, name) { (Self::TypeVar(typevar), "__name__") => Type::string_literal(db, typevar.name(db)), - (Self::TypeVar(typevar), "__bound__") => typevar - .upper_bound(db) - .map(|ty| ty.to_meta_type(db)) - .unwrap_or_else(|| KnownClass::NoneType.to_instance(db)), - (Self::TypeVar(typevar), "__constraints__") => { - let tuple_elements: Vec> = typevar - .constraints(db) - .unwrap_or_default() - .iter() - .map(|ty| ty.to_meta_type(db)) - .collect(); - Type::tuple(db, &tuple_elements) - } - (Self::TypeVar(typevar), "__default__") => typevar - .default_ty(db) - .map(|ty| ty.to_meta_type(db)) - .unwrap_or_else(|| KnownClass::NoDefaultType.to_instance(db)), (Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)), _ => return self.instance_fallback(db).member(db, name), }; @@ -2000,6 +1996,7 @@ pub struct TypeVarInstance<'db> { } impl<'db> TypeVarInstance<'db> { + #[allow(unused)] pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option> { if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) { Some(ty) @@ -2008,6 +2005,7 @@ impl<'db> TypeVarInstance<'db> { } } + #[allow(unused)] pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&[Type<'db>]> { if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) { Some(tuple.elements(db)) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 853abbe261..54215f1db3 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -6035,6 +6035,72 @@ mod tests { ); } + #[test] + fn pep695_type_params() { + let mut db = setup_db(); + + db.write_dedented( + "src/a.py", + " + def f[T, U: A, V: (A, B), W = A, X: A = A1, Y: (int,)](): + pass + + class A: ... + class B: ... + class A1(A): ... + ", + ) + .unwrap(); + + let check_typevar = |var: &'static str, + upper_bound: Option<&'static str>, + constraints: Option<&[&'static str]>, + default: Option<&'static str>| { + let var_ty = get_symbol(&db, "src/a.py", &["f"], var).expect_type(); + assert_eq!(var_ty.display(&db).to_string(), var); + + let expected_name_ty = format!(r#"Literal["{var}"]"#); + let name_ty = var_ty.member(&db, "__name__").expect_type(); + assert_eq!(name_ty.display(&db).to_string(), expected_name_ty); + + let KnownInstanceType::TypeVar(typevar) = var_ty.expect_known_instance() else { + panic!("expected TypeVar"); + }; + + assert_eq!( + typevar + .upper_bound(&db) + .map(|ty| ty.display(&db).to_string()), + upper_bound.map(std::borrow::ToOwned::to_owned) + ); + assert_eq!( + typevar.constraints(&db).map(|tys| tys + .iter() + .map(|ty| ty.display(&db).to_string()) + .collect::>()), + constraints.map(|strings| strings + .iter() + .map(std::string::ToString::to_string) + .collect::>()) + ); + assert_eq!( + typevar + .default_ty(&db) + .map(|ty| ty.display(&db).to_string()), + default.map(std::borrow::ToOwned::to_owned) + ); + }; + + check_typevar("T", None, None, None); + check_typevar("U", Some("A"), None, None); + check_typevar("V", None, Some(&["A", "B"]), None); + check_typevar("W", None, None, Some("A")); + check_typevar("X", Some("A"), None, Some("A1")); + + // a typevar with less than two constraints is treated as unconstrained + check_typevar("Y", None, None, None); + } + // Incremental inference tests fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> {