diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 354521288e..e6e6acd35c 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -670,3 +670,59 @@ reveal_type(with_parameters(int_int, 1)) # revealed: Overload[(x: int) -> str, # error: [invalid-argument-type] reveal_type(with_parameters(int_int, "a")) # revealed: Overload[(x: int) -> str, (x: str) -> str] ``` + +## ParamSpec attribute assignability + +When comparing signatures with `ParamSpec` attributes (`P.args` and `P.kwargs`), two different +inferable `ParamSpec` attributes with the same kind are assignable to each other. This enables +method overrides where both methods have their own `ParamSpec`. + +### Same attribute kind, both inferable + +```py +from typing import Callable + +class Parent: + def method[**P](self, callback: Callable[P, None]) -> Callable[P, None]: + return callback + +class Child1(Parent): + # This is a valid override: Q.args matches P.args, Q.kwargs matches P.kwargs + def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]: + return callback + +# Both signatures use ParamSpec, so they should be compatible +def outer[**P](f: Callable[P, int]) -> Callable[P, int]: + def inner[**Q](g: Callable[Q, int]) -> Callable[Q, int]: + return g + return inner(f) +``` + +We can explicitly mark it as an override using the `@override` decorator. + +```py +from typing import override + +class Child2(Parent): + @override + def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]: + return callback +``` + +### One `ParamSpec` not inferable + +Here, `P` is in a non-inferable position while `Q` is inferable. So, they are not considered +assignable. + +```py +from typing import Callable + +class Container[**P]: + def method(self, f: Callable[P, None]) -> Callable[P, None]: + return f + + def try_assign[**Q](self, f: Callable[Q, None]) -> Callable[Q, None]: + # error: [invalid-return-type] "Return type does not match returned value: expected `(**Q@try_assign) -> None`, found `(**P@Container) -> None`" + # error: [invalid-argument-type] "Argument to bound method `method` is incorrect: Expected `(**P@Container) -> None`, found `(**Q@try_assign) -> None`" + return self.method(f) +``` diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 45c3f81de2..76fe3a35d4 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -1072,6 +1072,29 @@ impl<'db> Signature<'db> { let mut check_types = |type1: Option>, type2: Option>| { let type1 = type1.unwrap_or(Type::unknown()); let type2 = type2.unwrap_or(Type::unknown()); + + match (type1, type2) { + // This is a special case where the _same_ components of two different `ParamSpec` + // type variables are assignable to each other when they're both in an inferable + // position. + // + // `ParamSpec` type variables can only occur in parameter lists so this special case + // is present here instead of in `Type::has_relation_to_impl`. + (Type::TypeVar(typevar1), Type::TypeVar(typevar2)) + if typevar1.paramspec_attr(db).is_some() + && typevar1.paramspec_attr(db) == typevar2.paramspec_attr(db) + && typevar1 + .without_paramspec_attr(db) + .is_inferable(db, inferable) + && typevar2 + .without_paramspec_attr(db) + .is_inferable(db, inferable) => + { + return true; + } + _ => {} + } + !result .intersect( db,