Skip to content

Conditional type-check based on callable call #2627

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 4 commits into from
Jan 11, 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: 76 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2506,6 +2506,76 @@ def conditional_type_map(expr: Expression,
return {}, {}


def partition_by_callable(type: Optional[Type]) -> Tuple[List[Type], List[Type]]:
"""Takes in a type and partitions that type into callable subtypes and
uncallable subtypes.

Thus, given:
`callables, uncallables = partition_by_callable(type)`

If we assert `callable(type)` then `type` has type Union[*callables], and
If we assert `not callable(type)` then `type` has type Union[*uncallables]

Guaranteed to not return [], []"""
if isinstance(type, FunctionLike) or isinstance(type, TypeType):
return [type], []

if isinstance(type, AnyType):
return [type], [type]

if isinstance(type, UnionType):
callables = []
uncallables = []
for subtype in type.items:
subcallables, subuncallables = partition_by_callable(subtype)
callables.extend(subcallables)
uncallables.extend(subuncallables)
return callables, uncallables

if isinstance(type, TypeVarType):
return partition_by_callable(type.erase_to_union_or_bound())

if isinstance(type, Instance):
method = type.type.get_method('__call__')
if method:
callables, uncallables = partition_by_callable(method.type)
if len(callables) and not len(uncallables):
# Only consider the type callable if its __call__ method is
# definitely callable.
return [type], []
return [], [type]

return [], [type]


def conditional_callable_type_map(expr: Expression,
current_type: Optional[Type],
) -> Tuple[TypeMap, TypeMap]:
"""Takes in an expression and the current type of the expression.

Returns a 2-tuple: The first element is a map from the expression to
the restricted type if it were callable. The second element is a
map from the expression to the type it would hold if it weren't
callable."""
if not current_type:
return {}, {}

if isinstance(current_type, AnyType):
return {}, {}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: This could be factored with less indentation by reversing the order of conditional checks. Also, avoid backslashes. I know you based off conditional_type_map() but given it's already copied and pasted, how about:

    if not current_type:
        return {}, {}
        
    if isinstance(current_type, CallableType):
        return {}, None

    if isinstance(current_type, UnionType):
        callables = [item for item in current_type.items
                     if isinstance(item, CallableType)]  # type: List[Type]
        non_callables = [item for item in current_type.items
                         if not isinstance(item, CallableType)]  # type: List[Type]
        return (
            {expr: UnionType.make_union(callables)},
            {expr: UnionType.make_union(non_callables)},
        )

    return None, {}

The two list comprehensions are a bit wasteful but I guess that's fine, we don't expect unions to be overly long.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reformatted. I also changed it to a for loop. I initially thought the comprehensions would be faster, but basic experiments show I'm wrong.


callables, uncallables = partition_by_callable(current_type)

if len(callables) and len(uncallables):
callable_map = {expr: UnionType.make_union(callables)} if len(callables) else None
uncallable_map = {expr: UnionType.make_union(uncallables)} if len(uncallables) else None
return callable_map, uncallable_map

elif len(callables):
return {}, None

return None, {}


def is_true_literal(n: Expression) -> bool:
return (refers_to_fullname(n, 'builtins.True')
or isinstance(n, IntExpr) and n.value == 1)
Expand Down Expand Up @@ -2579,7 +2649,7 @@ def find_isinstance_check(node: Expression,
type_map: Dict[Expression, Type],
) -> Tuple[TypeMap, TypeMap]:
"""Find any isinstance checks (within a chain of ands). Includes
implicit and explicit checks for None.
implicit and explicit checks for None and calls to callable.

Return value is a map of variables to their types if the condition
is true and a map of variables to their types if the condition is false.
Expand All @@ -2600,6 +2670,11 @@ def find_isinstance_check(node: Expression,
vartype = type_map[expr]
type = get_isinstance_type(node.args[1], type_map)
return conditional_type_map(expr, vartype, type)
elif refers_to_fullname(node.callee, 'builtins.callable'):
expr = node.args[0]
if expr.literal == LITERAL_TYPE:
vartype = type_map[expr]
return conditional_callable_type_map(expr, vartype)
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
1 change: 1 addition & 0 deletions mypy/test/testcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
# List of files that contain test case descriptions.
files = [
'check-basic.test',
'check-callable.test',
'check-classes.test',
'check-expressions.test',
'check-statements.test',
Expand Down
Loading