blob: ace6149bdc6b98ad8d9cf7b6cefba9d877e046b4 [file] [log] [blame]
"""Semantic analysis of NewType definitions.
This is conceptually part of mypy.semanal (semantic analyzer pass 2).
"""
from typing import Tuple, Optional
from mypy.types import (
Type, Instance, CallableType, NoneType, TupleType, AnyType, PlaceholderType,
TypeOfAny, get_proper_type
)
from mypy.nodes import (
AssignmentStmt, NewTypeExpr, CallExpr, NameExpr, RefExpr, Context, StrExpr, BytesExpr,
UnicodeExpr, Block, FuncDef, Argument, TypeInfo, Var, SymbolTableNode, MDEF, ARG_POS,
PlaceholderNode
)
from mypy.semanal_shared import SemanticAnalyzerInterface
from mypy.options import Options
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type
from mypy.messages import MessageBuilder, format_type
from mypy.errorcodes import ErrorCode
from mypy import errorcodes as codes
class NewTypeAnalyzer:
def __init__(self,
options: Options,
api: SemanticAnalyzerInterface,
msg: MessageBuilder) -> None:
self.options = options
self.api = api
self.msg = msg
def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
"""Check if s declares a NewType; if yes, store it in symbol table.
Return True if it's a NewType declaration. The current target may be
deferred as a side effect if the base type is not ready, even if
the return value is True.
The logic in this function mostly copies the logic for visit_class_def()
with a single (non-Generic) base.
"""
name, call = self.analyze_newtype_declaration(s)
if name is None or call is None:
return False
# OK, now we know this is a NewType. But the base type may be not ready yet,
# add placeholder as we do for ClassDef.
fullname = self.api.qualified_name(name)
if (not call.analyzed or
isinstance(call.analyzed, NewTypeExpr) and not call.analyzed.info):
# Start from labeling this as a future class, as we do for normal ClassDefs.
placeholder = PlaceholderNode(fullname, s, s.line, becomes_typeinfo=True)
self.api.add_symbol(name, placeholder, s, can_defer=False)
old_type, should_defer = self.check_newtype_args(name, call, s)
old_type = get_proper_type(old_type)
if not call.analyzed:
call.analyzed = NewTypeExpr(name, old_type, line=call.line, column=call.column)
if old_type is None:
if should_defer:
# Base type is not ready.
self.api.defer()
return True
# Create the corresponding class definition if the aliased type is subtypeable
if isinstance(old_type, TupleType):
newtype_class_info = self.build_newtype_typeinfo(name, old_type,
old_type.partial_fallback)
newtype_class_info.tuple_type = old_type
elif isinstance(old_type, Instance):
if old_type.type.is_protocol:
self.fail("NewType cannot be used with protocol classes", s)
newtype_class_info = self.build_newtype_typeinfo(name, old_type, old_type)
else:
if old_type is not None:
message = "Argument 2 to NewType(...) must be subclassable (got {})"
self.fail(message.format(format_type(old_type)), s, code=codes.VALID_NEWTYPE)
# Otherwise the error was already reported.
old_type = AnyType(TypeOfAny.from_error)
object_type = self.api.named_type('__builtins__.object')
newtype_class_info = self.build_newtype_typeinfo(name, old_type, object_type)
newtype_class_info.fallback_to_any = True
check_for_explicit_any(old_type, self.options, self.api.is_typeshed_stub_file, self.msg,
context=s)
if self.options.disallow_any_unimported and has_any_from_unimported_type(old_type):
self.msg.unimported_type_becomes_any("Argument 2 to NewType(...)", old_type, s)
# If so, add it to the symbol table.
assert isinstance(call.analyzed, NewTypeExpr)
# As we do for normal classes, create the TypeInfo only once, then just
# update base classes on next iterations (to get rid of placeholders there).
if not call.analyzed.info:
call.analyzed.info = newtype_class_info
else:
call.analyzed.info.bases = newtype_class_info.bases
self.api.add_symbol(name, call.analyzed.info, s)
newtype_class_info.line = s.line
return True
def analyze_newtype_declaration(self,
s: AssignmentStmt) -> Tuple[Optional[str], Optional[CallExpr]]:
"""Return the NewType call expression if `s` is a newtype declaration or None otherwise."""
name, call = None, None
if (len(s.lvalues) == 1
and isinstance(s.lvalues[0], NameExpr)
and isinstance(s.rvalue, CallExpr)
and isinstance(s.rvalue.callee, RefExpr)
and s.rvalue.callee.fullname == 'typing.NewType'):
name = s.lvalues[0].name
if s.type:
self.fail("Cannot declare the type of a NewType declaration", s)
names = self.api.current_symbol_table()
existing = names.get(name)
# Give a better error message than generic "Name already defined".
if (existing and
not isinstance(existing.node, PlaceholderNode) and not s.rvalue.analyzed):
self.fail("Cannot redefine '%s' as a NewType" % name, s)
# This dummy NewTypeExpr marks the call as sufficiently analyzed; it will be
# overwritten later with a fully complete NewTypeExpr if there are no other
# errors with the NewType() call.
call = s.rvalue
return name, call
def check_newtype_args(self, name: str, call: CallExpr,
context: Context) -> Tuple[Optional[Type], bool]:
"""Ananlyze base type in NewType call.
Return a tuple (type, should defer).
"""
has_failed = False
args, arg_kinds = call.args, call.arg_kinds
if len(args) != 2 or arg_kinds[0] != ARG_POS or arg_kinds[1] != ARG_POS:
self.fail("NewType(...) expects exactly two positional arguments", context)
return None, False
# Check first argument
if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)):
self.fail("Argument 1 to NewType(...) must be a string literal", context)
has_failed = True
elif args[0].value != name:
msg = "String argument 1 '{}' to NewType(...) does not match variable name '{}'"
self.fail(msg.format(args[0].value, name), context)
has_failed = True
# Check second argument
msg = "Argument 2 to NewType(...) must be a valid type"
try:
unanalyzed_type = expr_to_unanalyzed_type(args[1])
except TypeTranslationError:
self.fail(msg, context)
return None, False
# We want to use our custom error message (see above), so we suppress
# the default error message for invalid types here.
old_type = get_proper_type(self.api.anal_type(unanalyzed_type,
report_invalid_types=False))
should_defer = False
if old_type is None or isinstance(old_type, PlaceholderType):
should_defer = True
# The caller of this function assumes that if we return a Type, it's always
# a valid one. So, we translate AnyTypes created from errors into None.
if isinstance(old_type, AnyType) and old_type.is_from_error:
self.fail(msg, context)
return None, False
return None if has_failed else old_type, should_defer
def build_newtype_typeinfo(self, name: str, old_type: Type, base_type: Instance) -> TypeInfo:
info = self.api.basic_new_typeinfo(name, base_type)
info.is_newtype = True
# Add __init__ method
args = [Argument(Var('self'), NoneType(), None, ARG_POS),
self.make_argument('item', old_type)]
signature = CallableType(
arg_types=[Instance(info, []), old_type],
arg_kinds=[arg.kind for arg in args],
arg_names=['self', 'item'],
ret_type=NoneType(),
fallback=self.api.named_type('__builtins__.function'),
name=name)
init_func = FuncDef('__init__', args, Block([]), typ=signature)
init_func.info = info
init_func._fullname = self.api.qualified_name(name) + '.__init__'
info.names['__init__'] = SymbolTableNode(MDEF, init_func)
return info
# Helpers
def make_argument(self, name: str, type: Type) -> Argument:
return Argument(Var(name), type, None, ARG_POS)
def fail(self, msg: str, ctx: Context, *, code: Optional[ErrorCode] = None) -> None:
self.api.fail(msg, ctx, code=code)