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 7 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
61 changes: 38 additions & 23 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) \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use if (...)" to avoid the use of \.

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,22 @@ 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:
return None
elif len(types) == 1:
return types[0]
else:
return UnionType(types)
types.append(TypeRange(type, is_upper_bound=False))
elif isinstance(type, TypeType):
types.append(TypeRange(type.item, is_upper_bound=True))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add comment here describing why is_upper_bound=True.

else: # we didn't see an actual type, but rather a variable whose value is unknown to us
return None
assert len(types) != 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if we should return None if the length if 0? For example, what happens if we have isinstance(x, ())?

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
28 changes: 28 additions & 0 deletions test-data/unit/check-isinstance.test
Original file line number Diff line number Diff line change
Expand Up @@ -1360,3 +1360,31 @@ 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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Add also an else: block and do a reveal_type(x) in that block. Add another reveal_type(x) just after the if statement.

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]'
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here we could actually infer the type to be builtins.str, since we know that it can't be builtins.int due to the isinstance check.

[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, ())?


Copy link
Collaborator

Choose a reason for hiding this comment

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

Add test for the type object that is Type[A] or similar. Example:

class A: pass

def f(x: Union[int, A], a: Type[A]) -> None:
    if isinstance(x, a):
        reveal_type(x)
    elif isinstance(x, int):
        reveal_type(x)
    else:
        reveal_type(x)
    reveal_type(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