10000 Narrow types after 'in' operator by ilevkivskyi · Pull Request #4072 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Narrow types after 'in' operator #4072

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

Merged
merged 6 commits into from
Oct 13, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Limit narrowing after in only to optional types
  • Loading branch information
ilevkivskyi committed Oct 13, 2017
commit 59e69d6fee1cdf121cff2d9d0403988d0c1a32b0
30 changes: 16 additions & 14 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, join_type_list
from mypy.join import join_types
from mypy.treetransform import TransformVisitor
from mypy.binder import ConditionalTypeBinder, get_declaration
from mypy.meet import is_overlapping_types
Expand Down Expand Up @@ -2880,12 +2880,12 @@ def builtin_item_type(tp: Type) -> Optional[Type]:
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.
# TODO: fix tuple in lib-stub/builtins.pyi (it should be generic).
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)
return UnionType.make_simplified_union(tp.items) # this type is not externally visible
elif isinstance(tp, TypedDictType):
# TypedDict always has non-optional string keys.
if tp.fallback.type.fullname() == 'typing.Mapping':
Expand Down Expand Up @@ -3007,17 +3007,6 @@ 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) and
is_overlapping_types(item_type, type_map[expr]) and
not isinstance(type_map[expr], AnyType)):
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']
Expand Down Expand Up @@ -3052,6 +3041,19 @@ def find_isinstance_check(node: Expression,
optional_expr = node.operands[1]
if is_overlapping_types(optional_type, comp_type):
return {optional_expr: remove_optional(optional_type)}, {}
elif node.operators in [['in'], ['not in']]:
expr = node.operands[0]
left_type = type_map[expr]
right_type = builtin_item_type(type_map[node.operands[1]])
right_ok = right_type and (not is_optional(right_type) and
(not isinstance(right_type, Instance) or
right_type.type.fullname() != 'builtins.object'))
if (right_ok and is_optional(left_type) and literal(expr) == LITERAL_TYPE and
not is_literal_none(expr) and is_overlapping_types(left_type, right_type)):
if node.operators == ['in']:
return {expr: remove_optional(left_type)}, {}
if node.operators == ['not in']:
return {}, {expr: remove_optional(left_type)}
elif isinstance(node, RefExpr):
# Restrict the type of the variable to True-ish/False-ish in the if and else branches
# respectively
Expand Down
106 changes: 59 additions & 47 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -1771,136 +1771,148 @@ def narrow_any_to_str_then_reassign_to_int() -> None:
[builtins fixtures/isinstance.pyi]

[case testNarrowTypeAfterInList]
from typing import List
# flags: --strict-optional
from typing import List, Optional

x: List[int]
Copy link
Collaborator 10000

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

y: object
y: Optional[int]

if y in x:
reveal_type(y) # E: Revealed type is 'builtins.int'
else:
reveal_type(y) # E: Revealed type is 'builtins.object'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
if y not in x:
reveal_type(y) # E: Revealed type is 'builtins.object'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
else:
reveal_type(y) # E: Revealed type is 'builtins.int'
[builtins fixtures/list.pyi]
[out]

[case testNarrowTypeAfterInListNonOverlapping]
# flags: --strict-optional
from typing import List, Optional

x: List[str]
y: Optional[int]

if y in x:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
else:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
[builtins fixtures/list.pyi]
[out]

[case testNarrowTypeAfterInTuple]
# flags: --strict-optional
from typing import Optional
class A: pass
class B(A): pass
class C(A): pass

y: object
y: Optional[B]
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'
reveal_type(y) # E: Revealed type is '__main__.B'
else:
reveal_type(y) # E: Revealed type is '__main__.A'
reveal_type(y) # E: Revealed type is 'Union[__main__.B, builtins.None]'
[builtins fixtures/tuple.pyi]
[out]

[case testNarrowTypeAfterInNamedTuple]
from typing import NamedTuple
# flags: --strict-optional
from typing import NamedTuple, Optional
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'
y: Optional[int]
if y not in nt:
reveal_type(y) # E: Revealed type is 'builtins.object'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
else:
reveal_type(y) # E: Revealed type is 'builtins.int'
[builtins fixtures/tuple.pyi]
[out]

[case testNarrowTypeAfterInDict]
from typing import Dict, Union
# flags: --strict-optional
from typing import Dict, Optional
x: Dict[str, str]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

y: Union[int, str]
y: Optional[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]'
reveal_type(y) # E: Revealed type is 'Union[builtins.str, builtins.None]'
if y not in x:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
reveal_type(y) # E: Revealed type is 'Union[builtins.str, builtins.None]'
else:
reveal_type(y) # E: Revealed type is 'builtins.str'
[builtins fixtures/dict.pyi]
[out]

[case testNarrowTypeAfterInList_python2]
from typing import List
# flags: --strict-optional
from typing import List, Optional

x = None # type: List[int]
y = None # type: object
x = [] # type: List[int]
y = None # type: Optional[int]

# 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 'builtins.object'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
if y not in x: # type: ignore
reveal_type(y) # E: Revealed type is 'builtins.object'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
else:
reveal_type(y) # E: Revealed type is 'builtins.int'

[builtins_py2 fixtures/python2.pyi]
[out]

[case testNarrowTypeAfterInNoPromotionsOrAny]
from typing import Any, List
x: List[int]
[case testNarrowTypeAfterInNoAnyOrObject]
# flags: --strict-optional
from typing import Any, List, Optional
x: List[Any]
z: List[object]

y: Any
# We never narrow down from Any
y: Optional[int]
if y in x:
reveal_type(y) # E: Revealed type is 'Any'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
else:
reveal_type(y) # E: Revealed type is 'Any'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'

z: float
# We never use promotions
if z not in x:
reveal_type(z) # E: Revealed type is 'builtins.float'
if y not in z:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
else:
reveal_type(z) # E: Revealed type is 'builtins.float'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
[typing fixtures/typing-full.pyi]
[builtins fixtures/list.pyi]
[out]

[case testNarrowTypeAfterInUserDefined]
from typing import Container, Union
# flags: --strict-optional
from typing import Container, Optional

class C(Container[int]):
def __contains__(self, item: object) -> bool:
return item is 'surprise'

y: Union[int, str]
y: Optional[int]
# We never trust user defined types
if y in C():
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
else:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
if y not in C():
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
else:
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.str]'
reveal_type(y) # E: Revealed type is 'Union[builtins.int, builtins.None]'
[typing fixtures/typing-full.pyi]
[builtins fixtures/list.pyi]
[out]

[case testNarrowTypeAfterInStrictOptional]
[case testNarrowTypeAfterInSet]
# flags: --strict-optional
from typing import Optional, Set
s: Set[str]
Expand All @@ -1917,7 +1929,7 @@ else:
[builtins fixtures/set.pyi]
[out]

[case testNarrowTypeAfterInStrictOptional2]
[case testNarrowTypeAfterInTypedDict]
# flags: --strict-optional
from typing import Optional
from mypy_extensions import TypedDict
Expand Down
0