8000 Fix inference logic for isinstance by pkch · Pull Request #2997 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Fix inference logic for isinstance #2997

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 8 commits into from
Mar 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
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
77 changes: 50 additions & 27 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2452,28 +2452,46 @@ def push_type_map(self, type_map: Optional[Dict[Expression, Type]]) -> None:

TypeMap = Optional[Dict[Expression, Type]]

# An object that represents either a precise type or a type with an upper bound;
# it is important for correct type inference with isinstance.
TypeRange = NamedTuple(
'TypeRange',
[
('item', Type),
('is_upper_bound', bool), # False => precise type
])


def conditional_type_map(expr: Expression,
current_type: Optional[Type],
proposed_type: Optional[Type],
proposed_type_ranges: Optional[List[TypeRange]],
) -> Tuple[TypeMap, TypeMap]:
"""Takes in an expression, the current type of the expression, and a
proposed type of that expression.

Returns a 2-tuple: The first element is a map from the expression to
the proposed type, if the expression can be the proposed type. The
second element is a map from the expression to the type it would hold
if it was not the proposed type, if any."""
if proposed_type:
if it was not the proposed type, if any. None means bot, {} means top"""
if proposed_type_ranges:
if len(proposed_type_ranges) == 1:
proposed_type = proposed_type_ranges[0].item # Union with a single type breaks tests
else:
proposed_type = UnionType([type_range.item for type_range in proposed_type_ranges])
if current_type:
if is_proper_subtype(current_type, proposed_type):
# Expression is always of type proposed_type
if (not any(type_range.is_upper_bound for type_range in proposed_type_ranges)
and is_proper_subtype(current_type, proposed_type)):
# Expression is always of one of the types in proposed_type_ranges
return {}, None
elif not is_overlapping_types(current_type, proposed_type):
# Expression is never of type proposed_type
# Expression is never of any type in proposed_type_ranges
return None, {}
else:
remaining_type = restrict_subtype_away(current_type, proposed_type)
# we can only restrict when the type is precise, not bounded
proposed_precise_type = UnionType([type_range.item
for type_range in proposed_type_ranges
if not type_range.is_upper_bound])
remaining_type = restrict_subtype_away(current_type, proposed_precise_type)
return {expr: proposed_type}, {expr: remaining_type}
else:
return {expr: proposed_type}, {}
Expand Down Expand Up @@ -2644,8 +2662,8 @@ def find_isinstance_check(node: Expression,
expr = node.args[0]
if expr.literal == LITERAL_TYPE:
vartype = type_map[expr]
type = get_isinstance_type(node.args[1], type_map)
return conditional_type_map(expr, vartype, type)
types = get_isinstance_type(node.args[1], type_map)
return conditional_type_map(expr, vartype, types)
elif refers_to_fullname(node.callee, 'builtins.callable'):
expr = node.args[0]
if expr.literal == LITERAL_TYPE:
Expand All @@ -2663,7 +2681,8 @@ def find_isinstance_check(node: Expression,
# two elements in node.operands, and at least one of them
# should represent a None.
vartype = type_map[expr]
if_vars, else_vars = conditional_type_map(expr, vartype, NoneTyp())
none_typ = [TypeRange(NoneTyp(), is_upper_bound=False)]
if_vars, else_vars = conditional_type_map(expr, vartype, none_typ)
break

if is_not:
Expand Down Expand Up @@ -2725,26 +2744,30 @@ def flatten(t: Expression) -> List[Expression]:
return [t]


def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> Type:
def get_isinstance_type(expr: Expression, type_map: Dict[Expression, Type]) -> List[TypeRange]:
all_types = [type_map[e] for e in flatten(expr)]

types = [] # type: List[Type]

types = [] # type: List[TypeRange]
for type in all_types:
if isinstance(type, FunctionLike):
if type.is_type_obj():
# Type variables may be present -- erase them, which is the best
# we can do (outside disallowing them here).
type = erase_typevars(type.items()[0].ret_type)

types.append(type)

if len(types) == 0:
if isinstance(type, FunctionLike) and type.is_type_obj():
# Type variables may be present -- erase them, which is the best
# we can do (outside disallowing them here).
type = erase_typevars(type.items()[0].ret_type)
types.append(TypeRange(type, is_upper_bound=False))
elif isinstance(type, TypeType):
# Type[A] means "any type that is a subtype of A" rather than "precisely type A"
# we indicate this by setting is_upper_bound flag
types.append(TypeRange(type.item, is_upper_bound=True))
elif isinstance(type, Instance) and type.type.fullname() == 'builtins.type':
object_type = Instance(type.type.mro[-1], [])
types.append(TypeRange(object_type, is_upper_bound=True))
else: # we didn't see an actual type, but rather a variable whose value is unknown to us
return None
if not types:
# this can happen if someone has empty tuple as 2nd argument to isinstance
# strictly speaking, we should return UninhabitedType but for simplicity we will simply
# refuse to do any type inference for now
return None
elif len(types) == 1:
return types[0]
else:
return UnionType(types)
return types


def expand_func(defn: FuncItem, map: Dict[TypeVarId, Type]) -> FuncItem:
Expand Down
3 changes: 1 addition & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2679,8 +2679,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None:
# This branch handles the case foo.bar where foo is a module.
# In this case base.node is the module's MypyFile and we look up
# bar in its namespace. This must be done for all types of bar.
file = base.node
assert isinstance(file, (MypyFile, type(None)))
file = cast(Optional[MypyFile], base.node) # can't use isinstance due to issue #2999
n = file.names.get(expr.name, None) if file is not None else None
if n:
n = self.normalize_type_alias(n, expr)
Expand Down
52 changes: 52 additions & 0 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -1360,3 +1360,55 @@ def f(x: object) -> None:
reveal_type(b) # E: Revealed type is '__main__.A'
[builtins fixtures/isinstance.pyi]
[out]


[case testIsInstanceWithUnknownType]
from typing import Union
def f(x: Union[int, str], typ: type) -> None:
if isinstance(x, (typ, int)):
x + 1 # E: Unsupported operand types for + (likely involving Union)
reveal_type(x) # E: Revealed type is 'Union[builtins.int, builtins.str]'
else:
reveal_type(x) # E: Revealed type is 'builtins.str'
[builtins fixtures/isinstancelist.pyi]


[case testIsInstanceWithBoundedType]
from typing import Union, Type

class A: pass
def f(x: Union[int, A], a: Type[A]) -> None:
if isinstance(x, (a, int)):
reveal_type(x) # E: Revealed type is 'Union[builtins.int, __main__.A]'
else:
reveal_type(x) # E: Revealed type is '__main__.A'

[builtins fixtures/isinstancelist.pyi]


[case testIsInstanceWithEmtpy2ndArg]
from typing import Union
def f(x: Union[int, str]) -> None:
if isinstance(x, ()):
reveal_type(x) # E: Revealed type is 'Union[builtins.int, builtins.str]'
else:
reveal_type(x) # E: Revealed type is 'Union[builtins.int, builtins.str]'
[builtins fixtures/isinstancelist.pyi]


[case testIsInstanceWithTypeObject]
from typing import Union, Type

class A: pass

def f(x: Union[int, A], a: Type[A]) -> None:
if isinstance(x, a):
reveal_type(x) # E: Revealed type is '__main__.A'
elif isinstance(x, int):
reveal_type(x) # E: Revealed type is 'builtins.int'
else:
reveal_type(x) # E: Revealed type is '__main__.A'
reveal_type(x) # E: Revealed type is 'Union[builtins.int, __main__.A]'

[builtins fixtures/isinstancelist.pyi]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not directly related, but can you add a test case that does something like isinstance(x, ())?


2 changes: 1 addition & 1 deletion typeshed
Submodule typeshed updated 88 files
+2 −2 .travis.yml
+0 −1 CONTRIBUTING.md
+5 −5 requirements-tests-py3.txt
+11 −11 stdlib/2/ConfigParser.pyi
+21 −22 stdlib/2/__builtin__.pyi
+20 −20 stdlib/2/array.pyi
+10 −12 stdlib/2/codecs.pyi
+5 −8 stdlib/2/compileall.pyi
+9 −6 stdlib/2/datetime.pyi
+3 −3 stdlib/2/encodings/utf_8.pyi
+5 −5 stdlib/2/pickle.pyi
+1 −3 stdlib/2/socket.pyi
+2 −2 stdlib/2/spwd.pyi
+3 −3 stdlib/2/tempfile.pyi
+1 −1 stdlib/2/types.pyi
+40 −0 stdlib/2/unicodedata.pyi
+7 −7 stdlib/2and3/_bisect.pyi
+1 −1 stdlib/2and3/asynchat.pyi
+1 −1 stdlib/2and3/asyncore.pyi
+3 −3 stdlib/2and3/fractions.pyi
+0 −134 stdlib/2and3/ftplib.pyi
+3 −3 stdlib/2and3/logging/__init__.pyi
+1 −1 stdlib/2and3/logging/handlers.pyi
+2 −2 stdlib/2and3/opcode.pyi
+0 −38 stdlib/2and3/unicodedata.pyi
+28 −65 stdlib/3.4/asyncio/events.pyi
+5 −5 stdlib/3.4/asyncio/locks.pyi
+8 −8 stdlib/3.4/asyncio/queues.pyi
+1 −1 stdlib/3.4/asyncio/tasks.pyi
+2 −2 stdlib/3.4/pathlib.pyi
+0 −24 stdlib/3.4/statistics.pyi
+1 −1 stdlib/3/_markupbase.pyi
+4 −5 stdlib/3/_posixsubprocess.pyi
+1 −1 stdlib/3/base64.pyi
+20 −24 stdlib/3/builtins.pyi
+1 −1 stdlib/3/calendar.pyi
+194 −0 stdlib/3/codecs.pyi
+12 −13 stdlib/3/collections/__init__.pyi
+0 −18 stdlib/3/compileall.pyi
+1 −1 stdlib/3/datetime.pyi
+1 −1 stdlib/3/difflib.pyi
+45 −47 stdlib/3/dis.pyi
+1 −1 stdlib/3/email/message.pyi
+3 −3 stdlib/3/encodings/utf_8.pyi
+2 −3 stdlib/3/http/client.pyi
+1 −1 stdlib/3/inspect.pyi
+2 −2 stdlib/3/json.pyi
+3 −3 stdlib/3/linecache.pyi
+0 −6 stdlib/3/os/__init__.pyi
+1 −1 stdlib/3/queue.pyi
+2 −2 stdlib/3/shlex.pyi
+1 −3 stdlib/3/socket.pyi
+2 −0 stdlib/3/ssl.pyi
+41 −87 stdlib/3/subprocess.pyi
+39 −78 stdlib/3/tempfile.pyi
+3 −3 stdlib/3/token.pyi
+17 −30 stdlib/3/tokenize.pyi
+1 −1 stdlib/3/types.pyi
+5 −12 stdlib/3/typing.pyi
+37 −0 stdlib/3/unicodedata.pyi
+17 −18 stdlib/3/unittest/__init__.pyi
+2 −3 stdlib/3/urllib/parse.pyi
+2 −2 third_party/2/concurrent/futures/__init__.pyi
+1 −1 third_party/2/requests/packages/urllib3/connection.pyi
+1 −1 third_party/2/requests/packages/urllib3/packages/ssl_match_hostname/__init__.pyi
+55 −62 third_party/2/werkzeug/wrappers.pyi
+2 −2 third_party/2and3/characteristic/__init__.pyi
+2 −2 third_party/2and3/mypy_extensions.pyi
+5 −5 third_party/3.6/click/core.pyi
+13 −13 third_party/3.6/click/decorators.pyi
+4 −4 third_party/3.6/click/termui.pyi
+11 −11 third_party/3.6/click/types.pyi
+4 −4 third_party/3.6/click/utils.pyi
+4 −4 third_party/3/dateutil/parser.pyi
+2 −2 third_party/3/dateutil/relativedelta.pyi
+1 −1 third_party/3/dateutil/tz/_common.pyi
+1 −1 third_party/3/dateutil/tz/tz.pyi
+48 −49 third_party/3/itsdangerous.pyi
+24 −24 third_party/3/lxml/etree.pyi
+4 −4 third_party/3/requests/adapters.pyi
+17 −17 third_party/3/requests/api.pyi
+4 −6 third_party/3/requests/cookies.pyi
+4 −4 third_party/3/requests/models.pyi
+6 −13 third_party/3/requests/packages/urllib3/connection.pyi
+4 −2 third_party/3/requests/packages/urllib3/response.pyi
+22 −24 third_party/3/requests/sessions.pyi
+3 −3 third_party/3/requests/structures.pyi
+55 −62 third_party/3/werkzeug/wrappers.pyi
0