diff --git a/mypy/solve.py b/mypy/solve.py index e3709106996c..8ca0a5e7256b 100644 --- a/mypy/solve.py +++ b/mypy/solve.py @@ -9,7 +9,7 @@ from mypy.constraints import SUBTYPE_OF, SUPERTYPE_OF, Constraint, infer_constraints, neg_op from mypy.expandtype import expand_type from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort -from mypy.join import join_type_list +from mypy.join import is_similar_params, join_type_list from mypy.meet import meet_type_list, meet_types from mypy.subtypes import is_subtype from mypy.typeops import get_all_type_vars @@ -17,6 +17,7 @@ AnyType, Instance, NoneType, + Parameters, ParamSpecType, ProperType, TupleType, @@ -256,6 +257,13 @@ def _join_sorted_key(t: Type) -> int: return 0 +def _precise_parameter_constraint_target(target: Type) -> Parameters | None: + target = get_proper_type(target) + if isinstance(target, Parameters) and not target.imprecise_arg_kinds: + return target + return None + + def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: """Solve constraints by finding by using meets of upper bounds, and joins of lower bounds.""" @@ -288,6 +296,18 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: # Retain `None` when no bottoms were provided to avoid bogus `Never` inference. bottom = UnionType.make_union(lowers) else: + # Joining incompatible concrete ParamSpec lower bounds falls back to Any, + # but for precise call signatures this would silently erase the conflict. + precise_param_lowers = [ + lower + for lower in (_precise_parameter_constraint_target(lower) for lower in lowers) + if lower is not None + ] + if precise_param_lowers and not all( + is_similar_params(precise_param_lowers[0], lower) for lower in precise_param_lowers[1:] + ): + return None + # The order of lowers is non-deterministic. # We attempt to sort lowers because joins are non-associative. For instance: # join(join(int, str), int | str) == join(object, int | str) == object diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index 970ba45d0e8e..a9140a9c04b3 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -2556,6 +2556,42 @@ def fn(f: MiddlewareFactory[P]) -> Capture[P]: ... reveal_type(fn(ServerErrorMiddleware)) # N: Revealed type is "__main__.Capture[[handler: builtins.str | None =, debug: builtins.bool =]]" [builtins fixtures/paramspec.pyi] +[case testParamSpecProtocolInferenceRejectsConflictingMembers] +from typing import Generic, Protocol, TypeVar +from typing_extensions import ParamSpec + +P = ParamSpec("P") +T = TypeVar("T") + +class Context: pass + +class Namer(Protocol[P]): + def name_for(self, *args: P.args, **kwargs: P.kwargs) -> str: ... + def execute_on(self, ctx: Context, *args: P.args, **kwargs: P.kwargs) -> None: ... + +class Impl0: + def name_for(self, x: int, y: str) -> str: ... + def execute_on(self, ctx: Context, x: int, y: str) -> None: ... + +class Impl1: + def name_for(self, y: str) -> str: ... + def execute_on(self, ctx: Context, x: int, y: str) -> None: ... + +class UseImplFirst(Generic[P, T]): + def __init__(self, impl: T, *args: P.args, **kwargs: P.kwargs) -> None: ... + def __call__(self: "UseImplFirst[P, Namer[P]]") -> None: ... + +def use_impl_second(impl: Namer[P], *args: P.args, **kwargs: P.kwargs) -> None: ... +def use_impl_third(wrapper: UseImplFirst[P, Namer[P]]) -> None: ... + +ok: Namer[[int, str]] = Impl0() +use_impl_second(Impl0(), 0, "0") +use_impl_third(UseImplFirst(Impl0(), 0, "0")) + +use_impl_second(Impl1(), 1, "1") # E: Cannot infer value of type parameter "P" of "use_impl_second" +use_impl_third(UseImplFirst(Impl1(), 1, "1")) # E: Cannot infer value of type parameter "P" of "use_impl_third" +[builtins fixtures/paramspec.pyi] + [case testRunParamSpecDuplicateArgsKwargs] from typing_extensions import ParamSpec, Concatenate from typing import Callable, Union