Skip to content

Commit

Permalink
Special-case type inference of empty collections (#16122)
Browse files Browse the repository at this point in the history
Fixes #230
Fixes #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).
  • Loading branch information
ilevkivskyi authored Sep 26, 2023
1 parent 0c8b761 commit 4b66fa9
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 30 deletions.
14 changes: 14 additions & 0 deletions mypy/solve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ARG_STAR2,
CONTRAVARIANT,
COVARIANT,
INVARIANT,
Decorator,
FuncBase,
OverloadedFuncDef,
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion mypy/test/testpep561.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 2 additions & 9 deletions test-data/unit/check-inference-context.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
24 changes: 24 additions & 0 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
24 changes: 4 additions & 20 deletions test-data/unit/check-varargs.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit 4b66fa9

Please sign in to comment.