| """Semantic analysis of NewType definitions. |
| |
| This is conceptually part of mypy.semanal (semantic analyzer pass 2). |
| """ |
| |
| from __future__ import annotations |
| |
| from mypy import errorcodes as codes |
| from mypy.errorcodes import ErrorCode |
| from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type |
| from mypy.messages import MessageBuilder, format_type |
| from mypy.nodes import ( |
| ARG_POS, |
| MDEF, |
| Argument, |
| AssignmentStmt, |
| Block, |
| CallExpr, |
| Context, |
| FuncDef, |
| NameExpr, |
| NewTypeExpr, |
| PlaceholderNode, |
| RefExpr, |
| StrExpr, |
| SymbolTableNode, |
| TypeInfo, |
| Var, |
| ) |
| from mypy.options import Options |
| from mypy.semanal_shared import SemanticAnalyzerInterface, has_placeholder |
| from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type |
| from mypy.types import ( |
| AnyType, |
| CallableType, |
| Instance, |
| NoneType, |
| PlaceholderType, |
| TupleType, |
| Type, |
| TypeOfAny, |
| get_proper_type, |
| ) |
| |
| |
| 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. |
| """ |
| var_name, call = self.analyze_newtype_declaration(s) |
| if var_name is None or call is None: |
| return False |
| name = var_name |
| # OK, now we know this is a NewType. But the base type may be not ready yet, |
| # add placeholder as we do for ClassDef. |
| |
| if self.api.is_func_scope(): |
| name += "@" + str(s.line) |
| 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(var_name, placeholder, s, can_defer=False) |
| |
| old_type, should_defer = self.check_newtype_args(var_name, call, s) |
| old_type = get_proper_type(old_type) |
| if not isinstance(call.analyzed, NewTypeExpr): |
| call.analyzed = NewTypeExpr(var_name, old_type, line=call.line, column=call.column) |
| else: |
| call.analyzed.old_type = old_type |
| 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 |
| assert isinstance(call.analyzed, NewTypeExpr) |
| if isinstance(old_type, TupleType): |
| newtype_class_info = self.build_newtype_typeinfo( |
| name, old_type, old_type.partial_fallback, s.line, call.analyzed.info |
| ) |
| newtype_class_info.update_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, s.line, call.analyzed.info |
| ) |
| 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, s.line, call.analyzed.info |
| ) |
| 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(var_name, call.analyzed.info, s) |
| if self.api.is_func_scope(): |
| self.api.add_symbol_skip_local(name, call.analyzed.info) |
| newtype_class_info.line = s.line |
| return True |
| |
| def analyze_newtype_declaration(self, s: AssignmentStmt) -> tuple[str | None, CallExpr | None]: |
| """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(f'Cannot redefine "{name}" as a NewType', 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[Type | None, 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): |
| 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], self.options, self.api.is_stub_file) |
| 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, |
| allow_placeholder=not self.options.disable_recursive_aliases |
| and not self.api.is_func_scope(), |
| ) |
| ) |
| should_defer = False |
| if isinstance(old_type, PlaceholderType): |
| old_type = None |
| if old_type is None: |
| 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, |
| line: int, |
| existing_info: TypeInfo | None, |
| ) -> TypeInfo: |
| info = existing_info or self.api.basic_new_typeinfo(name, base_type, line) |
| info.bases = [base_type] # Update in case there were nested placeholders. |
| 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 = info.fullname + ".__init__" |
| if not existing_info: |
| updated = True |
| else: |
| previous_sym = info.names["__init__"].node |
| assert isinstance(previous_sym, FuncDef) |
| updated = old_type != previous_sym.arguments[1].variable.type |
| info.names["__init__"] = SymbolTableNode(MDEF, init_func) |
| |
| if has_placeholder(old_type): |
| self.api.process_placeholder(None, "NewType base", info, force_progress=updated) |
| 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: ErrorCode | None = None) -> None: |
| self.api.fail(msg, ctx, code=code) |