From 4b66fa9de07828621fee1d53abd533f3903e570a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 27 Sep 2023 00:29:11 +0100 Subject: [PATCH] Special-case type inference of empty collections (#16122) Fixes https://github.com/python/mypy/issues/230 Fixes https://github.com/python/mypy/issues/6463 I bet it fixes some other duplicates, I closed couple yesterday, but likely there are more. This may look a bit ad-hoc, but after some thinking this now starts to make sense to me for two reasons: * Unless I am missing something, this should be completely safe. Special-casing only applies to inferred types (i.e. empty collection literals etc). * Empty collections _are_ actually special. Even if we solve some classes of issues with more principled solutions (e.g. I want to re-work type inference against unions in near future), there will always be some corner cases involving empty collections. Similar issues keep coming, so I think it is a good idea to add this special-casing (especially taking into account how simple it is, and that it closer some "popular" issues). --- mypy/solve.py | 14 ++++++++++++ mypy/subtypes.py | 7 ++++++ mypy/test/testpep561.py | 2 +- test-data/unit/check-inference-context.test | 11 ++-------- test-data/unit/check-inference.test | 24 +++++++++++++++++++++ test-data/unit/check-varargs.test | 24 ++++----------------- 6 files changed, 52 insertions(+), 30 deletions(-) diff --git a/mypy/solve.py b/mypy/solve.py index 7cdf1c10c9b5..52e6549e98a6 100644 --- a/mypy/solve.py +++ b/mypy/solve.py @@ -239,6 +239,20 @@ def solve_one(lowers: Iterable[Type], uppers: Iterable[Type]) -> Type | None: top: Type | None = None candidate: Type | None = None + # Filter out previous results of failed inference, they will only spoil the current pass... + new_uppers = [] + for u in uppers: + pu = get_proper_type(u) + if not isinstance(pu, UninhabitedType) or not pu.ambiguous: + new_uppers.append(u) + uppers = new_uppers + + # ...unless this is the only information we have, then we just pass it on. + if not uppers and not lowers: + candidate = UninhabitedType() + candidate.ambiguous = True + return candidate + # Process each bound separately, and calculate the lower and upper # bounds based on constraints. Note that we assume that the constraint # targets do not have constraint references. diff --git a/mypy/subtypes.py b/mypy/subtypes.py index c5399db0a494..822c4b0ebf32 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -18,6 +18,7 @@ ARG_STAR2, CONTRAVARIANT, COVARIANT, + INVARIANT, Decorator, FuncBase, OverloadedFuncDef, @@ -342,6 +343,12 @@ def _is_subtype( def check_type_parameter( left: Type, right: Type, variance: int, proper_subtype: bool, subtype_context: SubtypeContext ) -> bool: + # It is safe to consider empty collection literals and similar as covariant, since + # such type can't be stored in a variable, see checker.is_valid_inferred_type(). + if variance == INVARIANT: + p_left = get_proper_type(left) + if isinstance(p_left, UninhabitedType) and p_left.ambiguous: + variance = COVARIANT if variance == COVARIANT: if proper_subtype: return is_proper_subtype(left, right, subtype_context=subtype_context) diff --git a/mypy/test/testpep561.py b/mypy/test/testpep561.py index 48d0658cd1e9..9d2628c1fa5f 100644 --- a/mypy/test/testpep561.py +++ b/mypy/test/testpep561.py @@ -131,7 +131,7 @@ def test_pep561(testcase: DataDrivenTestCase) -> None: steps = testcase.find_steps() if steps != [[]]: - steps = [[]] + steps # type: ignore[assignment] + steps = [[]] + steps for i, operations in enumerate(steps): perform_file_operations(operations) diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 169fee65f127..773a9ffd8274 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -1321,11 +1321,7 @@ from typing import List, TypeVar T = TypeVar('T', bound=int) def f(x: List[T]) -> List[T]: ... -# TODO: improve error message for such cases, see #3283 and #5706 -y: List[str] = f([]) \ - # E: Incompatible types in assignment (expression has type "List[Never]", variable has type "List[str]") \ - # N: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance \ - # N: Consider using "Sequence" instead, which is covariant +y: List[str] = f([]) [builtins fixtures/list.pyi] [case testWideOuterContextNoArgs] @@ -1342,10 +1338,7 @@ from typing import TypeVar, Optional, List T = TypeVar('T', bound=int) def f(x: Optional[T] = None) -> List[T]: ... -y: List[str] = f() \ - # E: Incompatible types in assignment (expression has type "List[Never]", variable has type "List[str]") \ - # N: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance \ - # N: Consider using "Sequence" instead, which is covariant +y: List[str] = f() [builtins fixtures/list.pyi] [case testUseCovariantGenericOuterContext] diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index f9a4d58c74af..caa44cb40ad4 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3686,3 +3686,27 @@ def g(*args: str) -> None: pass reveal_type(f(g)) # N: Revealed type is "Tuple[Never, Never]" \ # E: Argument 1 to "f" has incompatible type "Callable[[VarArg(str)], None]"; expected "Call[Never]" [builtins fixtures/list.pyi] + +[case testInferenceWorksWithEmptyCollectionsNested] +from typing import List, TypeVar, NoReturn +T = TypeVar('T') +def f(a: List[T], b: List[T]) -> T: pass +x = ["yes"] +reveal_type(f(x, [])) # N: Revealed type is "builtins.str" +reveal_type(f(["yes"], [])) # N: Revealed type is "builtins.str" + +empty: List[NoReturn] +f(x, empty) # E: Cannot infer type argument 1 of "f" +f(["no"], empty) # E: Cannot infer type argument 1 of "f" +[builtins fixtures/list.pyi] + +[case testInferenceWorksWithEmptyCollectionsUnion] +from typing import Any, Dict, NoReturn, NoReturn, Union + +def foo() -> Union[Dict[str, Any], Dict[int, Any]]: + return {} + +empty: Dict[NoReturn, NoReturn] +def bar() -> Union[Dict[str, Any], Dict[int, Any]]: + return empty +[builtins fixtures/dict.pyi] diff --git a/test-data/unit/check-varargs.test b/test-data/unit/check-varargs.test index 41668e991972..2495a883aa71 100644 --- a/test-data/unit/check-varargs.test +++ b/test-data/unit/check-varargs.test @@ -602,31 +602,15 @@ class A: pass class B: pass if int(): - a, aa = G().f(*[a]) \ - # E: Incompatible types in assignment (expression has type "List[A]", variable has type "A") \ - # E: Incompatible types in assignment (expression has type "List[Never]", variable has type "List[A]") \ - # N: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance \ - # N: Consider using "Sequence" instead, which is covariant - + a, aa = G().f(*[a]) # E: Incompatible types in assignment (expression has type "List[A]", variable has type "A") if int(): aa, a = G().f(*[a]) # E: Incompatible types in assignment (expression has type "List[Never]", variable has type "A") if int(): - ab, aa = G().f(*[a]) \ - # E: Incompatible types in assignment (expression has type "List[Never]", variable has type "List[A]") \ - # N: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance \ - # N: Consider using "Sequence" instead, which is covariant \ - # E: Argument 1 to "f" of "G" has incompatible type "*List[A]"; expected "B" - + ab, aa = G().f(*[a]) # E: Argument 1 to "f" of "G" has incompatible type "*List[A]"; expected "B" if int(): - ao, ao = G().f(*[a]) \ - # E: Incompatible types in assignment (expression has type "List[Never]", variable has type "List[object]") \ - # N: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance \ - # N: Consider using "Sequence" instead, which is covariant + ao, ao = G().f(*[a]) if int(): - aa, aa = G().f(*[a]) \ - # E: Incompatible types in assignment (expression has type "List[Never]", variable has type "List[A]") \ - # N: "List" is invariant -- see https://mypy.readthedocs.io/en/stable/common_issues.html#variance \ - # N: Consider using "Sequence" instead, which is covariant + aa, aa = G().f(*[a]) [builtins fixtures/list.pyi] [case testCallerTupleVarArgsAndGenericCalleeVarArg]