New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Narrow types after 'in' operator #4072

Merged
merged 6 commits into from Oct 13, 2017
Next

Narrow types after 'in' operator

  • Loading branch information...
ilevkivskyi committed Oct 8, 2017
commit 9b89f039207674dca42eeb6f739e31eb364419bd
View
@@ -53,7 +53,7 @@
from mypy.erasetype import erase_typevars
from mypy.expandtype import expand_type, expand_type_by_instance
from mypy.visitor import NodeVisitor
from mypy.join import join_types
from mypy.join import join_types, join_type_list
from mypy.treetransform import TransformVisitor
from mypy.binder import ConditionalTypeBinder, get_declaration
from mypy.meet import is_overlapping_types
@@ -2874,6 +2874,27 @@ def remove_optional(typ: Type) -> Type:
return typ
def builtin_item_type(tp: Type) -> Optional[Type]:
# This is only OK for built-in containers, where we know the behavior of __contains__.

This comment has been minimized.

@JukkaL

JukkaL Oct 13, 2017

Collaborator

Add docstring.

@JukkaL

JukkaL Oct 13, 2017

Collaborator

Add docstring.

if isinstance(tp, Instance):
if tp.type.fullname() in ['builtins.list', 'builtins.tuple', 'builtins.dict',
'builtins.set', 'builtins.frozenset']:
if not tp.args:
# TODO: make lib-stub/builtins.pyi define generic tuple.
return None
if not isinstance(tp.args[0], AnyType):
return tp.args[0]
elif isinstance(tp, TupleType) and all(not isinstance(it, AnyType) for it in tp.items):
return join_type_list(tp.items)
elif isinstance(tp, TypedDictType):
# TypedDict always has non-optional string keys.
if tp.fallback.type.fullname() == 'typing.Mapping':
return tp.fallback.args[0]
elif tp.fallback.type.bases[0].type.fullname() == 'typing.Mapping':
return tp.fallback.type.bases[0].args[0]
return None
def and_conditional_maps(m1: TypeMap, m2: TypeMap) -> TypeMap:
"""Calculate what information we can learn from the truth of (e1 and e2)
in terms of the information that we can learn from the truth of e1 and
@@ -2986,6 +3007,15 @@ def find_isinstance_check(node: Expression,
if literal(expr) == LITERAL_TYPE:
vartype = type_map[expr]
return conditional_callable_type_map(expr, vartype)
elif isinstance(node, ComparisonExpr) and node.operators in [['in'], ['not in']]:
expr = node.operands[0]
cont_type = type_map[node.operands[1]]
item_type = builtin_item_type(cont_type)
if item_type and literal(expr) == LITERAL_TYPE and not is_literal_none(expr):
if node.operators == ['in']:
return {expr: item_type}, {}
if node.operators == ['not in']:
return {}, {expr: item_type}
elif isinstance(node, ComparisonExpr) and experiments.STRICT_OPTIONAL:
# Check for `x is None` and `x is not None`.
is_not = node.operators == ['is not']
@@ -1757,7 +1757,6 @@ if isinstance(x, str, 1): # E: Too many arguments for "isinstance"
reveal_type(x) # E: Revealed type is 'builtins.int'
[builtins fixtures/isinstancelist.pyi]
[case testIsinstanceNarrowAny]
from typing import Any
@@ -1770,3 +1769,147 @@ def narrow_any_to_str_then_reassign_to_int() -> None:
reveal_type(v) # E: Revealed type is 'Any'
[builtins fixtures/isinstance.pyi]
[case testNarrowTypeAfterInList]
from typing import List, Any
x: List[int]

This comment has been minimized.

@JukkaL

JukkaL Oct 13, 2017

Collaborator

Add test for optional item type (e.g. List[Optional[int]]).

@JukkaL

JukkaL Oct 13, 2017

Collaborator

Add test for optional item type (e.g. List[Optional[int]]).

y: Any
if y in x:
reveal_type(y) # E: Revealed type is 'builtins.int'
else:
reveal_type(y) # E: Revealed type is 'Any'
if y not in x:
reveal_type(y) # E: Revealed type is 'Any'
else:
reveal_type(y) # E: Revealed type is 'builtins.int'
[builtins fixtures/list.pyi]
[out]
[case testNarrowTypeAfterInTuple]
class A: pass
class B(A): pass
class C(A): pass
y: object
if y in (B(), C()):
reveal_type(y) # E: Revealed type is '__main__.A'
else:
reveal_type(y) # E: Revealed type is 'builtins.object'
if y not in (B(), C()):
reveal_type(y) # E: Revealed type is 'builtins.object'
else:
reveal_type(y) # E: Revealed type is '__main__.A'
[builtins fixtures/tuple.pyi]
[out]
[case testNarrowTypeAfterInNamedTuple]
from typing import NamedTuple
class NT(NamedTuple):
x: int
y: int
nt: NT
y: object
if y in nt:
reveal_type(y) # E: Revealed type is 'builtins.int'
else:
reveal_type(y) # E: Revealed type is 'builtins.object'
if y not in nt:
reveal_type(y) # E: Revealed type is 'builtins.object'
else:
reveal_type(y) # E: Revealed type is 'builtins.int'
[builtins fixtures/tuple.pyi]
[out]
[case testNarrowTypeAfterInDict]
from typing import Dict, Union
x: Dict[str, str]

This comment has been minimized.

@JukkaL

JukkaL Oct 13, 2017

Collaborator

Use a different type for values to make sure that this works against the key type.

@JukkaL

JukkaL Oct 13, 2017

Collaborator

Use a different type for values to make sure that this works against the key type.

y: Union[int, str]
if y in x:
reveal_type(y) # E: Revealed type is 'builtins.str'
else:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
if y not in x:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
else:
reveal_type(y) # E: Revealed type is 'builtins.str'
[builtins fixtures/dict.pyi]
[out]
[case testNarrowTypeAfterInList_python2]
from typing import List, Any
x = None # type: List[int]
y = None # type: Any
# TODO: Fix running tests on Python 2: "Iterator[int]" has no attribute "next"
if y in x: # type: ignore
reveal_type(y) # E: Revealed type is 'builtins.int'
else:
reveal_type(y) # E: Revealed type is 'Any'
if y not in x: # type: ignore
reveal_type(y) # E: Revealed type is 'Any'
else:
reveal_type(y) # E: Revealed type is 'builtins.int'
[builtins_py2 fixtures/python2.pyi]
[out]
[case testNarrowTypeAfterInUserDefined]
from typing import Container, Union
class C(Container[int]):
def __contains__(self, item: object) -> bool:
return item is 'surprise'
y: Union[int, str]
# We never trust user defined types
if y in C():
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
else:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
if y not in C():
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
else:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
[typing fixtures/typing-full.pyi]
[builtins fixtures/list.pyi]
[out]
[case testNarrowTypeAfterInStrictOptional]
# flags: --strict-optional
from typing import Optional, Set
s: Set[str]
y: Optional[str]
if y in {'a', 'b', 'c'}:
reveal_type(y) # E: Revealed type is 'builtins.str'
else:
reveal_type(y) # E: Revealed type is 'Union[builtins.str, builtins.None]'
if y not in s:
reveal_type(y) # E: Revealed type is 'Union[builtins.str, builtins.None]'
else:
reveal_type(y) # E: Revealed type is 'builtins.str'
[builtins fixtures/set.pyi]
[out]
[case testNarrowTypeAfterInStrictOptional2]
# flags: --strict-optional
from typing import Optional
from mypy_extensions import TypedDict
class TD(TypedDict):
a: int
b: str
td: TD
def f() -> None:
x: Optional[str]
if x not in td:
return
reveal_type(x) # E: Revealed type is 'builtins.str'
[typing fixtures/typing-full.pyi]
[builtins fixtures/dict.pyi]
[out]
@@ -19,6 +19,7 @@ class dict(Generic[KT, VT]):
def __getitem__(self, key: KT) -> VT: pass
def __setitem__(self, k: KT, v: VT) -> None: pass
def __iter__(self) -> Iterator[KT]: pass
def __contains__(self, item: object) -> bool: pass
def update(self, a: Mapping[KT, VT]) -> None: pass
@overload
def get(self, k: KT) -> Optional[VT]: pass
@@ -11,6 +11,7 @@ class function: pass
class int: pass
class str: pass
class unicode: pass
class bool: pass
T = TypeVar('T')
class list(Iterable[T], Generic[T]): pass
@@ -13,9 +13,11 @@ class function: pass
class int: pass
class str: pass
class bool: pass
class set(Iterable[T], Generic[T]):
def __iter__(self) -> Iterator[T]: pass
def __contains__(self, item: object) -> bool: pass
def add(self, x: T) -> None: pass
def discard(self, x: T) -> None: pass
def update(self, x: Set[T]) -> None: pass
@@ -12,6 +12,7 @@ class type:
def __call__(self, *a) -> object: pass
class tuple(Sequence[Tco], Generic[Tco]):
def __iter__(self) -> Iterator[Tco]: pass
def __contains__(self, item: object) -> bool: pass
def __getitem__(self, x: int) -> Tco: pass
def count(self, obj: Any) -> int: pass
class function: pass
@@ -126,6 +126,7 @@ class Mapping(Iterable[T], Protocol[T, T_co]):
def get(self, k: T, default: Union[T_co, V]) -> Union[T_co, V]: pass
def values(self) -> Iterable[T_co]: pass # Approximate return type
def __len__(self) -> int: ...
def __contains__(self, arg: object) -> int: pass
@runtime
class MutableMapping(Mapping[T, U], Protocol):
ProTip! Use n and p to navigate between commits in a pull request.