From 5d365a1c556935ba2ec9e3b27f785d4c1b968046 Mon Sep 17 00:00:00 2001 From: MestreY0d4-Uninter <241404605+MestreY0d4-Uninter@users.noreply.github.com> Date: Sat, 2 May 2026 20:08:50 +0000 Subject: [PATCH 1/2] Avoid widening incompatible ParamSpec constraints Refs #21384. --- mypy/solve.py | 23 +++++++++++- .../unit/check-parameter-specification.test | 36 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/mypy/solve.py b/mypy/solve.py index e3709106996cd..410ce144b0d2d 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,19 @@ 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 970ba45d0e8e2..a9140a9c04b33 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 From c89f34c155459cd7ffcc41f953f48f5b47f15cff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 20:10:45 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/solve.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/solve.py b/mypy/solve.py index 410ce144b0d2d..8ca0a5e7256b7 100644 --- a/mypy/solve.py +++ b/mypy/solve.py @@ -304,8 +304,7 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: 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:] + is_similar_params(precise_param_lowers[0], lower) for lower in precise_param_lowers[1:] ): return None