Skip to content

Implementing background infrastructure for recursive types: Part 2 #7366

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 14 commits into from
Aug 22, 2019
111 changes: 86 additions & 25 deletions misc/proper_plugin.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
from mypy.plugin import Plugin, FunctionContext
from mypy.types import Type, Instance, CallableType, UnionType, get_proper_type
from mypy.types import (
Type, Instance, CallableType, UnionType, get_proper_type, ProperType,
get_proper_types, TupleType, NoneTyp, AnyType
)
from mypy.nodes import TypeInfo
from mypy.subtypes import is_proper_subtype

import os.path
from typing_extensions import Type as typing_Type
from typing import Optional, Callable

FILE_WHITELIST = [
'checker.py',
'checkexpr.py',
'checkmember.py',
'messages.py',
'semanal.py',
'typeanal.py'
]


class ProperTypePlugin(Plugin):
"""
Expand All @@ -31,30 +26,53 @@ def get_function_hook(self, fullname: str
) -> Optional[Callable[[FunctionContext], Type]]:
if fullname == 'builtins.isinstance':
return isinstance_proper_hook
if fullname == 'mypy.types.get_proper_type':
return proper_type_hook
if fullname == 'mypy.types.get_proper_types':
return proper_types_hook
return None


def isinstance_proper_hook(ctx: FunctionContext) -> Type:
if os.path.split(ctx.api.path)[-1] in FILE_WHITELIST:
return ctx.default_return_type
right = get_proper_type(ctx.arg_types[1][0])
for arg in ctx.arg_types[0]:
if is_improper_type(arg):
right = get_proper_type(ctx.arg_types[1][0])
if isinstance(right, CallableType) and right.is_type_obj():
if right.type_object().fullname() in ('mypy.types.Type',
'mypy.types.ProperType',
'mypy.types.TypeAliasType'):
# Special case: things like assert isinstance(typ, ProperType) are always OK.
return ctx.default_return_type
if right.type_object().fullname() in ('mypy.types.UnboundType',
'mypy.types.TypeVarType'):
# Special case: these are not valid targets for a type alias and thus safe.
return ctx.default_return_type
if (is_improper_type(arg) or
isinstance(get_proper_type(arg), AnyType) and is_dangerous_target(right)):
if is_special_target(right):
return ctx.default_return_type
Copy link
Collaborator

Choose a reason for hiding this comment

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

Idea: we could also maybe add a check for when the left operand is Any and the right operand is some Type. Could be overkill/out-of-scope for this PR.

ctx.api.fail('Never apply isinstance() to unexpanded types;'
' use mypy.types.get_proper_type() first', ctx.context)
return ctx.default_return_type


def is_special_target(right: ProperType) -> bool:
"""Whitelist some special cases for use in isinstance() with improper types."""
if isinstance(right, CallableType) and right.is_type_obj():
if right.type_object().fullname() == 'builtins.tuple':
# Used with Union[Type, Tuple[Type, ...]].
return True
if right.type_object().fullname() in ('mypy.types.Type',
'mypy.types.ProperType',
'mypy.types.TypeAliasType'):
# Special case: things like assert isinstance(typ, ProperType) are always OK.
return True
if right.type_object().fullname() in ('mypy.types.UnboundType',
'mypy.types.TypeVarType',
'mypy.types.RawExpressionType',
'mypy.types.EllipsisType',
'mypy.types.StarType',
'mypy.types.TypeList',
'mypy.types.CallableArgument',
'mypy.types.PartialType',
'mypy.types.ErasedType'):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we also want to include RawExpressionType and PlaceholderType to this list? I'm guessing "yes" for the former and "no" for the latter? (genuinely not sure).

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree.

# Special case: these are not valid targets for a type alias and thus safe.
# TODO: introduce a SyntheticType base to simplify this?
return True
elif isinstance(right, TupleType):
return all(is_special_target(t) for t in get_proper_types(right.items))
return False


def is_improper_type(typ: Type) -> bool:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we also consider 'Any' to be an improper type? (Could be overkill/out-of-scope for this PR)

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this and below actually makes sense. I will try and if it is not hard, I will add to this PR.

"""Is this a type that is not a subtype of ProperType?"""
typ = get_proper_type(typ)
Expand All @@ -66,5 +84,48 @@ def is_improper_type(typ: Type) -> bool:
return False


def is_dangerous_target(typ: ProperType) -> bool:
"""Is this a dangerous target (right argument) for an isinstance() check?"""
if isinstance(typ, TupleType):
return any(is_dangerous_target(get_proper_type(t)) for t in typ.items)
if isinstance(typ, CallableType) and typ.is_type_obj():
return typ.type_object().has_base('mypy.types.Type')
return False


def proper_type_hook(ctx: FunctionContext) -> Type:
"""Check if this get_proper_type() call is not redundant."""
arg_types = ctx.arg_types[0]
if arg_types:
arg_type = get_proper_type(arg_types[0])
proper_type = get_proper_type_instance(ctx)
if is_proper_subtype(arg_type, UnionType.make_union([NoneTyp(), proper_type])):
# Minimize amount of spurious errors from overload machinery.
# TODO: call the hook on the overload as a whole?
if isinstance(arg_type, (UnionType, Instance)):
ctx.api.fail('Redundant call to get_proper_type()', ctx.context)
return ctx.default_return_type


def proper_types_hook(ctx: FunctionContext) -> Type:
"""Check if this get_proper_types() call is not redundant."""
arg_types = ctx.arg_types[0]
if arg_types:
arg_type = arg_types[0]
proper_type = get_proper_type_instance(ctx)
item_type = UnionType.make_union([NoneTyp(), proper_type])
ok_type = ctx.api.named_generic_type('typing.Iterable', [item_type])
if is_proper_subtype(arg_type, ok_type):
ctx.api.fail('Redundant call to get_proper_types()', ctx.context)
return ctx.default_return_type


def get_proper_type_instance(ctx: FunctionContext) -> Instance:
types = ctx.api.modules['mypy.types'] # type: ignore
proper_type_info = types.names['ProperType']
assert isinstance(proper_type_info.node, TypeInfo)
return Instance(proper_type_info.node, [])


def plugin(version: str) -> typing_Type[ProperTypePlugin]:
return ProperTypePlugin
Loading