From 1feabc84fcba10412434d9bae42e146e35c9262f Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 26 Feb 2023 19:25:43 +0000 Subject: [PATCH 1/5] Add a failing test case --- test-data/unit/pythoneval.test | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index fbbaecbba241..08710bd74f9b 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1924,3 +1924,15 @@ _testStarUnpackNestedUnderscore.py:10: error: List item 0 has incompatible type _testStarUnpackNestedUnderscore.py:10: error: List item 1 has incompatible type "int"; expected "str" _testStarUnpackNestedUnderscore.py:11: note: Revealed type is "builtins.list[builtins.str]" _testStarUnpackNestedUnderscore.py:16: note: Revealed type is "builtins.list[builtins.object]" + +[case testStrictEqualitywithParamSpec] +# flags: --strict-equality +from typing import Generic +from typing_extensions import ParamSpec + +P = ParamSpec("P") + +class Foo(Generic[P]): ... + +def check(foo1: Foo[[int]], foo2: Foo[[str]]) -> bool: + return foo1 == foo2 From c2dc532ea159007b7d1eaa4a10caa80edaec6efe Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Sun, 26 Feb 2023 20:26:17 +0000 Subject: [PATCH 2/5] Fix `--strict-equality` crash for instances of a class generic over a `ParamSpec` --- mypy/meet.py | 29 +++++++++++++++++++++++++- test-data/unit/pythoneval.test | 38 ++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/mypy/meet.py b/mypy/meet.py index d99e1a92d2eb..52c70bd024c7 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -1,5 +1,6 @@ from __future__ import annotations +from itertools import chain from typing import Callable from mypy import join @@ -342,7 +343,15 @@ def _is_overlapping_types(left: Type, right: Type) -> bool: left_possible = get_possible_variants(left) right_possible = get_possible_variants(right) - # We start by checking multi-variant types like Unions first. We also perform + # First handle a special case: comparing a `Parameters` to a `ParamSpecType`. + # This should always be considered an overlapping equality check. + # This needs to be done before we move on to other TypeVarLike comparisons. + if (isinstance(left, Parameters) and isinstance(right, ParamSpecType)) or ( + isinstance(left, ParamSpecType) and isinstance(right, Parameters) + ): + return True + + # Now move on to checking multi-variant types like Unions. We also perform # the same logic if either type happens to be a TypeVar/ParamSpec/TypeVarTuple. # # Handling the TypeVarLikes now lets us simulate having them bind to the corresponding @@ -451,6 +460,24 @@ def _type_object_overlap(left: Type, right: Type) -> bool: elif isinstance(right, CallableType): right = right.fallback + if isinstance(left, Parameters): + if not isinstance(right, Parameters): + return False + if len(left.arg_types) == len(right.arg_types): + return all( + _is_overlapping_types(left_arg, right_arg) + for left_arg, right_arg in zip(left.arg_types, right.arg_types) + ) + if not any( + isinstance(arg, TypeVarLikeType) for arg in chain(left.arg_types, right.arg_types) + ): + return False + # TODO: Is this sound? + return True + if isinstance(right, Parameters): + assert not isinstance(left, (Parameters, TypeVarLikeType)) + return False + if isinstance(left, LiteralType) and isinstance(right, LiteralType): if left.value == right.value: # If values are the same, we still need to check if fallbacks are overlapping, diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index 08710bd74f9b..eccf4ba67cab 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1928,11 +1928,45 @@ _testStarUnpackNestedUnderscore.py:16: note: Revealed type is "builtins.list[bui [case testStrictEqualitywithParamSpec] # flags: --strict-equality from typing import Generic -from typing_extensions import ParamSpec +from typing_extensions import Concatenate, ParamSpec P = ParamSpec("P") class Foo(Generic[P]): ... +class Bar(Generic[P]): ... -def check(foo1: Foo[[int]], foo2: Foo[[str]]) -> bool: +def bad1(foo1: Foo[[int]], foo2: Foo[[str]]) -> bool: return foo1 == foo2 + +def bad2(foo1: Foo[[int, str]], foo2: Foo[[int, bytes]]) -> bool: + return foo1 == foo2 + +def bad3(foo1: Foo[[int]], foo2: Foo[[int, int]]) -> bool: + return foo1 == foo2 + +def bad4(foo: Foo[[int]], bar: Bar[[int]]) -> bool: + return foo == bar + +def good1(foo1: Foo[[int]], foo2: Foo[[int]]) -> bool: + return foo1 == foo2 + +def good2(foo1: Foo[[int]], foo2: Foo[[bool]]) -> bool: + return foo1 == foo2 + +def good3(foo1: Foo[[int, int]], foo2: Foo[[bool, bool]]) -> bool: + return foo1 == foo2 + +def good4(foo1: Foo[[int]], foo2: Foo[P], *args: P.args, **kwargs: P.kwargs) -> bool: + return foo1 == foo2 + +def good5(foo1: Foo[P], foo2: Foo[[int, str, bytes]], *args: P.args, **kwargs: P.kwargs) -> bool: + return foo1 == foo2 + +def good6(foo1: Foo[Concatenate[int, P]], foo2: Foo[[int, str, bytes]], *args: P.args, **kwargs: P.kwargs) -> bool: + return foo1 == foo2 + +[out] +_testStrictEqualitywithParamSpec.py:11: error: Non-overlapping equality check (left operand type: "Foo[[int]]", right operand type: "Foo[[str]]") +_testStrictEqualitywithParamSpec.py:14: error: Non-overlapping equality check (left operand type: "Foo[[int, str]]", right operand type: "Foo[[int, bytes]]") +_testStrictEqualitywithParamSpec.py:17: error: Non-overlapping equality check (left operand type: "Foo[[int]]", right operand type: "Foo[[int, int]]") +_testStrictEqualitywithParamSpec.py:20: error: Non-overlapping equality check (left operand type: "Foo[[int]]", right operand type: "Bar[[int]]") From e1ff0ed18680229feac8fce467a7b55093469a8b Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 27 Feb 2023 14:30:33 +0000 Subject: [PATCH 3/5] Address review; simplify --- mypy/meet.py | 36 ++++++++++++---------------------- test-data/unit/pythoneval.test | 29 ++++++++++++--------------- 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/mypy/meet.py b/mypy/meet.py index 52c70bd024c7..bfc43901c230 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -1,6 +1,5 @@ from __future__ import annotations -from itertools import chain from typing import Callable from mypy import join @@ -343,13 +342,20 @@ def _is_overlapping_types(left: Type, right: Type) -> bool: left_possible = get_possible_variants(left) right_possible = get_possible_variants(right) - # First handle a special case: comparing a `Parameters` to a `ParamSpecType`. - # This should always be considered an overlapping equality check. - # This needs to be done before we move on to other TypeVarLike comparisons. - if (isinstance(left, Parameters) and isinstance(right, ParamSpecType)) or ( - isinstance(left, ParamSpecType) and isinstance(right, Parameters) + # First handle special cases relating to PEP 612: + # - comparing a `Parameters` to a `Parameters + # - comparing a `Parameters` to a `ParamSpecType` + # - comparing a `ParamSpecType` to a `ParamSpecType` + # + # This should all always be considered overlapping equality checks. + # These need to be done before we move on to other TypeVarLike comparisons. + if isinstance(left, (Parameters, ParamSpecType)) and isinstance( + right, (Parameters, ParamSpecType) ): return True + # A `Parameters` does not overlap with anything else, however + if isinstance(left, Parameters) or isinstance(right, Parameters): + return False # Now move on to checking multi-variant types like Unions. We also perform # the same logic if either type happens to be a TypeVar/ParamSpec/TypeVarTuple. @@ -460,24 +466,6 @@ def _type_object_overlap(left: Type, right: Type) -> bool: elif isinstance(right, CallableType): right = right.fallback - if isinstance(left, Parameters): - if not isinstance(right, Parameters): - return False - if len(left.arg_types) == len(right.arg_types): - return all( - _is_overlapping_types(left_arg, right_arg) - for left_arg, right_arg in zip(left.arg_types, right.arg_types) - ) - if not any( - isinstance(arg, TypeVarLikeType) for arg in chain(left.arg_types, right.arg_types) - ): - return False - # TODO: Is this sound? - return True - if isinstance(right, Parameters): - assert not isinstance(left, (Parameters, TypeVarLikeType)) - return False - if isinstance(left, LiteralType) and isinstance(right, LiteralType): if left.value == right.value: # If values are the same, we still need to check if fallbacks are overlapping, diff --git a/test-data/unit/pythoneval.test b/test-data/unit/pythoneval.test index eccf4ba67cab..a3413e071184 100644 --- a/test-data/unit/pythoneval.test +++ b/test-data/unit/pythoneval.test @@ -1935,38 +1935,35 @@ P = ParamSpec("P") class Foo(Generic[P]): ... class Bar(Generic[P]): ... -def bad1(foo1: Foo[[int]], foo2: Foo[[str]]) -> bool: - return foo1 == foo2 +def bad(foo: Foo[[int]], bar: Bar[[int]]) -> bool: + return foo == bar -def bad2(foo1: Foo[[int, str]], foo2: Foo[[int, bytes]]) -> bool: +def good1(foo1: Foo[[int]], foo2: Foo[[str]]) -> bool: return foo1 == foo2 -def bad3(foo1: Foo[[int]], foo2: Foo[[int, int]]) -> bool: +def good2(foo1: Foo[[int, str]], foo2: Foo[[int, bytes]]) -> bool: return foo1 == foo2 -def bad4(foo: Foo[[int]], bar: Bar[[int]]) -> bool: - return foo == bar +def good3(foo1: Foo[[int]], foo2: Foo[[int, int]]) -> bool: + return foo1 == foo2 -def good1(foo1: Foo[[int]], foo2: Foo[[int]]) -> bool: +def good4(foo1: Foo[[int]], foo2: Foo[[int]]) -> bool: return foo1 == foo2 -def good2(foo1: Foo[[int]], foo2: Foo[[bool]]) -> bool: +def good5(foo1: Foo[[int]], foo2: Foo[[bool]]) -> bool: return foo1 == foo2 -def good3(foo1: Foo[[int, int]], foo2: Foo[[bool, bool]]) -> bool: +def good6(foo1: Foo[[int, int]], foo2: Foo[[bool, bool]]) -> bool: return foo1 == foo2 -def good4(foo1: Foo[[int]], foo2: Foo[P], *args: P.args, **kwargs: P.kwargs) -> bool: +def good7(foo1: Foo[[int]], foo2: Foo[P], *args: P.args, **kwargs: P.kwargs) -> bool: return foo1 == foo2 -def good5(foo1: Foo[P], foo2: Foo[[int, str, bytes]], *args: P.args, **kwargs: P.kwargs) -> bool: +def good8(foo1: Foo[P], foo2: Foo[[int, str, bytes]], *args: P.args, **kwargs: P.kwargs) -> bool: return foo1 == foo2 -def good6(foo1: Foo[Concatenate[int, P]], foo2: Foo[[int, str, bytes]], *args: P.args, **kwargs: P.kwargs) -> bool: +def good9(foo1: Foo[Concatenate[int, P]], foo2: Foo[[int, str, bytes]], *args: P.args, **kwargs: P.kwargs) -> bool: return foo1 == foo2 [out] -_testStrictEqualitywithParamSpec.py:11: error: Non-overlapping equality check (left operand type: "Foo[[int]]", right operand type: "Foo[[str]]") -_testStrictEqualitywithParamSpec.py:14: error: Non-overlapping equality check (left operand type: "Foo[[int, str]]", right operand type: "Foo[[int, bytes]]") -_testStrictEqualitywithParamSpec.py:17: error: Non-overlapping equality check (left operand type: "Foo[[int]]", right operand type: "Foo[[int, int]]") -_testStrictEqualitywithParamSpec.py:20: error: Non-overlapping equality check (left operand type: "Foo[[int]]", right operand type: "Bar[[int]]") +_testStrictEqualitywithParamSpec.py:11: error: Non-overlapping equality check (left operand type: "Foo[[int]]", right operand type: "Bar[[int]]") From a3a69021c807e319fc65bcced6a4257581e2dbef Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 27 Feb 2023 14:35:43 +0000 Subject: [PATCH 4/5] nit --- mypy/meet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/meet.py b/mypy/meet.py index bfc43901c230..e3c85b0f256c 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -343,7 +343,7 @@ def _is_overlapping_types(left: Type, right: Type) -> bool: right_possible = get_possible_variants(right) # First handle special cases relating to PEP 612: - # - comparing a `Parameters` to a `Parameters + # - comparing a `Parameters` to a `Parameters` # - comparing a `Parameters` to a `ParamSpecType` # - comparing a `ParamSpecType` to a `ParamSpecType` # From 9a5a41167fe9d170bf7c49f95483803f54f2274a Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 27 Feb 2023 14:42:35 +0000 Subject: [PATCH 5/5] ugh grammar --- mypy/meet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/meet.py b/mypy/meet.py index e3c85b0f256c..3214b4b43975 100644 --- a/mypy/meet.py +++ b/mypy/meet.py @@ -347,7 +347,7 @@ def _is_overlapping_types(left: Type, right: Type) -> bool: # - comparing a `Parameters` to a `ParamSpecType` # - comparing a `ParamSpecType` to a `ParamSpecType` # - # This should all always be considered overlapping equality checks. + # These should all always be considered overlapping equality checks. # These need to be done before we move on to other TypeVarLike comparisons. if isinstance(left, (Parameters, ParamSpecType)) and isinstance( right, (Parameters, ParamSpecType)