-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
Changes from all commits
4378fb8
56bd189
8dae216
f8f674d
b19b49f
0783653
9dd8c9d
3aa3dbd
76bddb8
72a9b08
a1eef72
f26ffc7
0e889c4
42f0c7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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): | ||
""" | ||
|
@@ -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 | ||
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'): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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 |
There was a problem hiding this comment.
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.