Repository URL to install this package:
|
Version:
0.630 ▾
|
mypy
/
plugin.py
|
|---|
"""Plugin system for extending mypy."""
from abc import abstractmethod
from functools import partial
from typing import Callable, List, Tuple, Optional, NamedTuple, TypeVar, Dict
from mypy_extensions import trait
from mypy.nodes import (
Expression, StrExpr, IntExpr, UnaryExpr, Context, DictExpr, ClassDef,
TypeInfo, SymbolTableNode, MypyFile
)
from mypy.tvar_scope import TypeVarScope
from mypy.types import (
Type, Instance, CallableType, TypedDictType, UnionType, NoneTyp, TypeVarType,
AnyType, TypeList, UnboundType, TypeOfAny
)
from mypy.messages import MessageBuilder
from mypy.options import Options
@trait
class TypeAnalyzerPluginInterface:
"""Interface for accessing semantic analyzer functionality in plugins."""
@abstractmethod
def fail(self, msg: str, ctx: Context) -> None:
raise NotImplementedError
@abstractmethod
def named_type(self, name: str, args: List[Type]) -> Instance:
raise NotImplementedError
@abstractmethod
def analyze_type(self, typ: Type) -> Type:
raise NotImplementedError
@abstractmethod
def analyze_callable_args(self, arglist: TypeList) -> Optional[Tuple[List[Type],
List[int],
List[Optional[str]]]]:
raise NotImplementedError
# A context for a hook that semantically analyzes an unbound type.
AnalyzeTypeContext = NamedTuple(
'AnalyzeTypeContext', [
('type', UnboundType), # Type to analyze
('context', Context),
('api', TypeAnalyzerPluginInterface)])
@trait
class CheckerPluginInterface:
"""Interface for accessing type checker functionality in plugins."""
msg = None # type: MessageBuilder
@abstractmethod
def named_generic_type(self, name: str, args: List[Type]) -> Instance:
raise NotImplementedError
@trait
class SemanticAnalyzerPluginInterface:
"""Interface for accessing semantic analyzer functionality in plugins."""
modules = None # type: Dict[str, MypyFile]
options = None # type: Options
msg = None # type: MessageBuilder
@abstractmethod
def named_type(self, qualified_name: str, args: Optional[List[Type]] = None) -> Instance:
raise NotImplementedError
@abstractmethod
def parse_bool(self, expr: Expression) -> Optional[bool]:
raise NotImplementedError
@abstractmethod
def fail(self, msg: str, ctx: Context, serious: bool = False, *,
blocker: bool = False) -> None:
raise NotImplementedError
@abstractmethod
def anal_type(self, t: Type, *,
tvar_scope: Optional[TypeVarScope] = None,
allow_tuple_literal: bool = False,
allow_unbound_tvars: bool = False,
third_pass: bool = False) -> Type:
raise NotImplementedError
@abstractmethod
def class_type(self, info: TypeInfo) -> Type:
raise NotImplementedError
@abstractmethod
def builtin_type(self, fully_qualified_name: str) -> Instance:
raise NotImplementedError
@abstractmethod
def lookup_fully_qualified(self, name: str) -> SymbolTableNode:
raise NotImplementedError
@abstractmethod
def lookup_fully_qualified_or_none(self, name: str) -> Optional[SymbolTableNode]:
raise NotImplementedError
@abstractmethod
def lookup_qualified(self, name: str, ctx: Context,
suppress_errors: bool = False) -> Optional[SymbolTableNode]:
raise NotImplementedError
# A context for a function hook that infers the return type of a function with
# a special signature.
#
# A no-op callback would just return the inferred return type, but a useful
# callback at least sometimes can infer a more precise type.
FunctionContext = NamedTuple(
'FunctionContext', [
('arg_types', List[List[Type]]), # List of actual caller types for each formal argument
('default_return_type', Type), # Return type inferred from signature
('args', List[List[Expression]]), # Actual expressions for each formal argument
('context', Context),
('api', CheckerPluginInterface)])
# A context for a method signature hook that infers a better signature for a
# method. Note that argument types aren't available yet. If you need them,
# you have to use a method hook instead.
MethodSigContext = NamedTuple(
'MethodSigContext', [
('type', Type), # Base object type for method call
('args', List[List[Expression]]), # Actual expressions for each formal argument
('default_signature', CallableType), # Original signature of the method
('context', Context),
('api', CheckerPluginInterface)])
# A context for a method hook that infers the return type of a method with a
# special signature.
#
# This is very similar to FunctionContext (only differences are documented).
MethodContext = NamedTuple(
'MethodContext', [
('type', Type), # Base object type for method call
('arg_types', List[List[Type]]),
('default_return_type', Type),
('args', List[List[Expression]]),
('context', Context),
('api', CheckerPluginInterface)])
# A context for an attribute type hook that infers the type of an attribute.
AttributeContext = NamedTuple(
'AttributeContext', [
('type', Type), # Type of object with attribute
('default_attr_type', Type), # Original attribute type
('context', Context),
('api', CheckerPluginInterface)])
# A context for a class hook that modifies the class definition.
ClassDefContext = NamedTuple(
'ClassDecoratorContext', [
('cls', ClassDef), # The class definition
('reason', Expression), # The expression being applied (decorator, metaclass, base class)
('api', SemanticAnalyzerPluginInterface)
])
class Plugin:
"""Base class of all type checker plugins.
This defines a no-op plugin. Subclasses can override some methods to
provide some actual functionality.
All get_ methods are treated as pure functions (you should assume that
results might be cached).
Look at the comments of various *Context objects for descriptions of
various hooks.
"""
def __init__(self, options: Options) -> None:
self.options = options
self.python_version = options.python_version
def get_type_analyze_hook(self, fullname: str
) -> Optional[Callable[[AnalyzeTypeContext], Type]]:
return None
def get_function_hook(self, fullname: str
) -> Optional[Callable[[FunctionContext], Type]]:
return None
def get_method_signature_hook(self, fullname: str
) -> Optional[Callable[[MethodSigContext], CallableType]]:
return None
def get_method_hook(self, fullname: str
) -> Optional[Callable[[MethodContext], Type]]:
return None
def get_attribute_hook(self, fullname: str
) -> Optional[Callable[[AttributeContext], Type]]:
return None
def get_class_decorator_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return None
def get_metaclass_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return None
def get_base_class_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return None
def get_customize_class_mro_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return None
T = TypeVar('T')
class ChainedPlugin(Plugin):
"""A plugin that represents a sequence of chained plugins.
Each lookup method returns the hook for the first plugin that
reports a match.
This class should not be subclassed -- use Plugin as the base class
for all plugins.
"""
# TODO: Support caching of lookup results (through a LRU cache, for example).
def __init__(self, options: Options, plugins: List[Plugin]) -> None:
"""Initialize chained plugin.
Assume that the child plugins aren't mutated (results may be cached).
"""
super().__init__(options)
self._plugins = plugins
def get_type_analyze_hook(self, fullname: str
) -> Optional[Callable[[AnalyzeTypeContext], Type]]:
return self._find_hook(lambda plugin: plugin.get_type_analyze_hook(fullname))
def get_function_hook(self, fullname: str
) -> Optional[Callable[[FunctionContext], Type]]:
return self._find_hook(lambda plugin: plugin.get_function_hook(fullname))
def get_method_signature_hook(self, fullname: str
) -> Optional[Callable[[MethodSigContext], CallableType]]:
return self._find_hook(lambda plugin: plugin.get_method_signature_hook(fullname))
def get_method_hook(self, fullname: str
) -> Optional[Callable[[MethodContext], Type]]:
return self._find_hook(lambda plugin: plugin.get_method_hook(fullname))
def get_attribute_hook(self, fullname: str
) -> Optional[Callable[[AttributeContext], Type]]:
return self._find_hook(lambda plugin: plugin.get_attribute_hook(fullname))
def get_class_decorator_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return self._find_hook(lambda plugin: plugin.get_class_decorator_hook(fullname))
def get_metaclass_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return self._find_hook(lambda plugin: plugin.get_metaclass_hook(fullname))
def get_base_class_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return self._find_hook(lambda plugin: plugin.get_base_class_hook(fullname))
def get_customize_class_mro_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
return self._find_hook(lambda plugin: plugin.get_customize_class_mro_hook(fullname))
def _find_hook(self, lookup: Callable[[Plugin], T]) -> Optional[T]:
for plugin in self._plugins:
hook = lookup(plugin)
if hook:
return hook
return None
class DefaultPlugin(Plugin):
"""Type checker plugin that is enabled by default."""
def get_function_hook(self, fullname: str
) -> Optional[Callable[[FunctionContext], Type]]:
if fullname == 'contextlib.contextmanager':
return contextmanager_callback
elif fullname == 'builtins.open' and self.python_version[0] == 3:
return open_callback
return None
def get_method_signature_hook(self, fullname: str
) -> Optional[Callable[[MethodSigContext], CallableType]]:
if fullname == 'typing.Mapping.get':
return typed_dict_get_signature_callback
return None
def get_method_hook(self, fullname: str
) -> Optional[Callable[[MethodContext], Type]]:
if fullname == 'typing.Mapping.get':
return typed_dict_get_callback
elif fullname == 'builtins.int.__pow__':
return int_pow_callback
return None
def get_class_decorator_hook(self, fullname: str
) -> Optional[Callable[[ClassDefContext], None]]:
from mypy.plugins import attrs
from mypy.plugins import dataclasses
if fullname in attrs.attr_class_makers:
return attrs.attr_class_maker_callback
elif fullname in attrs.attr_dataclass_makers:
return partial(
attrs.attr_class_maker_callback,
auto_attribs_default=True
)
elif fullname in dataclasses.dataclass_makers:
return dataclasses.dataclass_class_maker_callback
return None
def open_callback(ctx: FunctionContext) -> Type:
"""Infer a better return type for 'open'.
Infer TextIO or BinaryIO as the return value if the mode argument is not
given or is a literal.
"""
mode = None
if not ctx.arg_types or len(ctx.arg_types[1]) != 1:
mode = 'r'
elif isinstance(ctx.args[1][0], StrExpr):
mode = ctx.args[1][0].value
if mode is not None:
assert isinstance(ctx.default_return_type, Instance)
if 'b' in mode:
return ctx.api.named_generic_type('typing.BinaryIO', [])
else:
return ctx.api.named_generic_type('typing.TextIO', [])
return ctx.default_return_type
def contextmanager_callback(ctx: FunctionContext) -> Type:
"""Infer a better return type for 'contextlib.contextmanager'."""
# Be defensive, just in case.
if ctx.arg_types and len(ctx.arg_types[0]) == 1:
arg_type = ctx.arg_types[0][0]
if (isinstance(arg_type, CallableType)
and isinstance(ctx.default_return_type, CallableType)):
# The stub signature doesn't preserve information about arguments so
# add them back here.
return ctx.default_return_type.copy_modified(
arg_types=arg_type.arg_types,
arg_kinds=arg_type.arg_kinds,
arg_names=arg_type.arg_names,
variables=arg_type.variables,
is_ellipsis_args=arg_type.is_ellipsis_args)
return ctx.default_return_type
def typed_dict_get_signature_callback(ctx: MethodSigContext) -> CallableType:
"""Try to infer a better signature type for TypedDict.get.
This is used to get better type context for the second argument that
depends on a TypedDict value type.
"""
signature = ctx.default_signature
if (isinstance(ctx.type, TypedDictType)
and len(ctx.args) == 2
and len(ctx.args[0]) == 1
and isinstance(ctx.args[0][0], StrExpr)
and len(signature.arg_types) == 2
and len(signature.variables) == 1
and len(ctx.args[1]) == 1):
key = ctx.args[0][0].value
value_type = ctx.type.items.get(key)
ret_type = signature.ret_type
if value_type:
default_arg = ctx.args[1][0]
if (isinstance(value_type, TypedDictType)
and isinstance(default_arg, DictExpr)
and len(default_arg.items) == 0):
# Caller has empty dict {} as default for typed dict.
value_type = value_type.copy_modified(required_keys=set())
# Tweak the signature to include the value type as context. It's
# only needed for type inference since there's a union with a type
# variable that accepts everything.
tv = TypeVarType(signature.variables[0])
return signature.copy_modified(
arg_types=[signature.arg_types[0],
UnionType.make_simplified_union([value_type, tv])],
ret_type=ret_type)
return signature
def typed_dict_get_callback(ctx: MethodContext) -> Type:
"""Infer a precise return type for TypedDict.get with literal first argument."""
if (isinstance(ctx.type, TypedDictType)
and len(ctx.arg_types) >= 1
and len(ctx.arg_types[0]) == 1):
if isinstance(ctx.args[0][0], StrExpr):
key = ctx.args[0][0].value
value_type = ctx.type.items.get(key)
if value_type:
if len(ctx.arg_types) == 1:
return UnionType.make_simplified_union([value_type, NoneTyp()])
elif (len(ctx.arg_types) == 2 and len(ctx.arg_types[1]) == 1
and len(ctx.args[1]) == 1):
default_arg = ctx.args[1][0]
if (isinstance(default_arg, DictExpr) and len(default_arg.items) == 0
and isinstance(value_type, TypedDictType)):
# Special case '{}' as the default for a typed dict type.
return value_type.copy_modified(required_keys=set())
else:
return UnionType.make_simplified_union([value_type, ctx.arg_types[1][0]])
else:
ctx.api.msg.typeddict_key_not_found(ctx.type, key, ctx.context)
return AnyType(TypeOfAny.from_error)
return ctx.default_return_type
def int_pow_callback(ctx: MethodContext) -> Type:
"""Infer a more precise return type for int.__pow__."""
if (len(ctx.arg_types) == 1
and len(ctx.arg_types[0]) == 1):
arg = ctx.args[0][0]
if isinstance(arg, IntExpr):
exponent = arg.value
elif isinstance(arg, UnaryExpr) and arg.op == '-' and isinstance(arg.expr, IntExpr):
exponent = -arg.expr.value
else:
# Right operand not an int literal or a negated literal -- give up.
return ctx.default_return_type
if exponent >= 0:
return ctx.api.named_generic_type('builtins.int', [])
else:
return ctx.api.named_generic_type('builtins.float', [])
return ctx.default_return_type