665 lines
28 KiB
Python
665 lines
28 KiB
Python
|
"""Semantic analysis of named tuple definitions.
|
||
|
|
||
|
This is conceptually part of mypy.semanal.
|
||
|
"""
|
||
|
|
||
|
from __future__ import annotations
|
||
|
|
||
|
from contextlib import contextmanager
|
||
|
from typing import Final, Iterator, List, Mapping, cast
|
||
|
|
||
|
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
|
||
|
from mypy.nodes import (
|
||
|
ARG_NAMED_OPT,
|
||
|
ARG_OPT,
|
||
|
ARG_POS,
|
||
|
MDEF,
|
||
|
Argument,
|
||
|
AssignmentStmt,
|
||
|
Block,
|
||
|
CallExpr,
|
||
|
ClassDef,
|
||
|
Context,
|
||
|
Decorator,
|
||
|
EllipsisExpr,
|
||
|
Expression,
|
||
|
ExpressionStmt,
|
||
|
FuncBase,
|
||
|
FuncDef,
|
||
|
ListExpr,
|
||
|
NamedTupleExpr,
|
||
|
NameExpr,
|
||
|
PassStmt,
|
||
|
RefExpr,
|
||
|
Statement,
|
||
|
StrExpr,
|
||
|
SymbolTable,
|
||
|
SymbolTableNode,
|
||
|
TempNode,
|
||
|
TupleExpr,
|
||
|
TypeInfo,
|
||
|
TypeVarExpr,
|
||
|
Var,
|
||
|
is_StrExpr_list,
|
||
|
)
|
||
|
from mypy.options import Options
|
||
|
from mypy.semanal_shared import (
|
||
|
PRIORITY_FALLBACKS,
|
||
|
SemanticAnalyzerInterface,
|
||
|
calculate_tuple_fallback,
|
||
|
has_placeholder,
|
||
|
set_callable_name,
|
||
|
)
|
||
|
from mypy.types import (
|
||
|
TYPED_NAMEDTUPLE_NAMES,
|
||
|
AnyType,
|
||
|
CallableType,
|
||
|
LiteralType,
|
||
|
TupleType,
|
||
|
Type,
|
||
|
TypeOfAny,
|
||
|
TypeType,
|
||
|
TypeVarLikeType,
|
||
|
TypeVarType,
|
||
|
UnboundType,
|
||
|
has_type_vars,
|
||
|
)
|
||
|
from mypy.util import get_unique_redefinition_name
|
||
|
|
||
|
# Matches "_prohibited" in typing.py, but adds __annotations__, which works at runtime but can't
|
||
|
# easily be supported in a static checker.
|
||
|
NAMEDTUPLE_PROHIBITED_NAMES: Final = (
|
||
|
"__new__",
|
||
|
"__init__",
|
||
|
"__slots__",
|
||
|
"__getnewargs__",
|
||
|
"_fields",
|
||
|
"_field_defaults",
|
||
|
"_field_types",
|
||
|
"_make",
|
||
|
"_replace",
|
||
|
"_asdict",
|
||
|
"_source",
|
||
|
"__annotations__",
|
||
|
)
|
||
|
|
||
|
NAMEDTUP_CLASS_ERROR: Final = (
|
||
|
"Invalid statement in NamedTuple definition; " 'expected "field_name: field_type [= default]"'
|
||
|
)
|
||
|
|
||
|
SELF_TVAR_NAME: Final = "_NT"
|
||
|
|
||
|
|
||
|
class NamedTupleAnalyzer:
|
||
|
def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
|
||
|
self.options = options
|
||
|
self.api = api
|
||
|
|
||
|
def analyze_namedtuple_classdef(
|
||
|
self, defn: ClassDef, is_stub_file: bool, is_func_scope: bool
|
||
|
) -> tuple[bool, TypeInfo | None]:
|
||
|
"""Analyze if given class definition can be a named tuple definition.
|
||
|
|
||
|
Return a tuple where first item indicates whether this can possibly be a named tuple,
|
||
|
and the second item is the corresponding TypeInfo (may be None if not ready and should be
|
||
|
deferred).
|
||
|
"""
|
||
|
for base_expr in defn.base_type_exprs:
|
||
|
if isinstance(base_expr, RefExpr):
|
||
|
self.api.accept(base_expr)
|
||
|
if base_expr.fullname in TYPED_NAMEDTUPLE_NAMES:
|
||
|
result = self.check_namedtuple_classdef(defn, is_stub_file)
|
||
|
if result is None:
|
||
|
# This is a valid named tuple, but some types are incomplete.
|
||
|
return True, None
|
||
|
items, types, default_items, statements = result
|
||
|
if is_func_scope and "@" not in defn.name:
|
||
|
defn.name += "@" + str(defn.line)
|
||
|
existing_info = None
|
||
|
if isinstance(defn.analyzed, NamedTupleExpr):
|
||
|
existing_info = defn.analyzed.info
|
||
|
info = self.build_namedtuple_typeinfo(
|
||
|
defn.name, items, types, default_items, defn.line, existing_info
|
||
|
)
|
||
|
defn.analyzed = NamedTupleExpr(info, is_typed=True)
|
||
|
defn.analyzed.line = defn.line
|
||
|
defn.analyzed.column = defn.column
|
||
|
defn.defs.body = statements
|
||
|
# All done: this is a valid named tuple with all types known.
|
||
|
return True, info
|
||
|
# This can't be a valid named tuple.
|
||
|
return False, None
|
||
|
|
||
|
def check_namedtuple_classdef(
|
||
|
self, defn: ClassDef, is_stub_file: bool
|
||
|
) -> tuple[list[str], list[Type], dict[str, Expression], list[Statement]] | None:
|
||
|
"""Parse and validate fields in named tuple class definition.
|
||
|
|
||
|
Return a four tuple:
|
||
|
* field names
|
||
|
* field types
|
||
|
* field default values
|
||
|
* valid statements
|
||
|
or None, if any of the types are not ready.
|
||
|
"""
|
||
|
if len(defn.base_type_exprs) > 1:
|
||
|
self.fail("NamedTuple should be a single base", defn)
|
||
|
items: list[str] = []
|
||
|
types: list[Type] = []
|
||
|
default_items: dict[str, Expression] = {}
|
||
|
statements: list[Statement] = []
|
||
|
for stmt in defn.defs.body:
|
||
|
statements.append(stmt)
|
||
|
if not isinstance(stmt, AssignmentStmt):
|
||
|
# Still allow pass or ... (for empty namedtuples).
|
||
|
if isinstance(stmt, PassStmt) or (
|
||
|
isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, EllipsisExpr)
|
||
|
):
|
||
|
continue
|
||
|
# Also allow methods, including decorated ones.
|
||
|
if isinstance(stmt, (Decorator, FuncBase)):
|
||
|
continue
|
||
|
# And docstrings.
|
||
|
if isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr):
|
||
|
continue
|
||
|
statements.pop()
|
||
|
defn.removed_statements.append(stmt)
|
||
|
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
|
||
|
elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
|
||
|
# An assignment, but an invalid one.
|
||
|
statements.pop()
|
||
|
defn.removed_statements.append(stmt)
|
||
|
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
|
||
|
else:
|
||
|
# Append name and type in this case...
|
||
|
name = stmt.lvalues[0].name
|
||
|
items.append(name)
|
||
|
if stmt.type is None:
|
||
|
types.append(AnyType(TypeOfAny.unannotated))
|
||
|
else:
|
||
|
# We never allow recursive types at function scope. Although it is
|
||
|
# possible to support this for named tuples, it is still tricky, and
|
||
|
# it would be inconsistent with type aliases.
|
||
|
analyzed = self.api.anal_type(
|
||
|
stmt.type,
|
||
|
allow_placeholder=not self.options.disable_recursive_aliases
|
||
|
and not self.api.is_func_scope(),
|
||
|
prohibit_self_type="NamedTuple item type",
|
||
|
)
|
||
|
if analyzed is None:
|
||
|
# Something is incomplete. We need to defer this named tuple.
|
||
|
return None
|
||
|
types.append(analyzed)
|
||
|
# ...despite possible minor failures that allow further analyzis.
|
||
|
if name.startswith("_"):
|
||
|
self.fail(
|
||
|
f"NamedTuple field name cannot start with an underscore: {name}", stmt
|
||
|
)
|
||
|
if stmt.type is None or hasattr(stmt, "new_syntax") and not stmt.new_syntax:
|
||
|
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
|
||
|
elif isinstance(stmt.rvalue, TempNode):
|
||
|
# x: int assigns rvalue to TempNode(AnyType())
|
||
|
if default_items:
|
||
|
self.fail(
|
||
|
"Non-default NamedTuple fields cannot follow default fields", stmt
|
||
|
)
|
||
|
else:
|
||
|
default_items[name] = stmt.rvalue
|
||
|
return items, types, default_items, statements
|
||
|
|
||
|
def check_namedtuple(
|
||
|
self, node: Expression, var_name: str | None, is_func_scope: bool
|
||
|
) -> tuple[str | None, TypeInfo | None, list[TypeVarLikeType]]:
|
||
|
"""Check if a call defines a namedtuple.
|
||
|
|
||
|
The optional var_name argument is the name of the variable to
|
||
|
which this is assigned, if any.
|
||
|
|
||
|
Return a tuple of two items:
|
||
|
* Internal name of the named tuple (e.g. the name passed as an argument to namedtuple)
|
||
|
or None if it is not a valid named tuple
|
||
|
* Corresponding TypeInfo, or None if not ready.
|
||
|
|
||
|
If the definition is invalid but looks like a namedtuple,
|
||
|
report errors but return (some) TypeInfo.
|
||
|
"""
|
||
|
if not isinstance(node, CallExpr):
|
||
|
return None, None, []
|
||
|
call = node
|
||
|
callee = call.callee
|
||
|
if not isinstance(callee, RefExpr):
|
||
|
return None, None, []
|
||
|
fullname = callee.fullname
|
||
|
if fullname == "collections.namedtuple":
|
||
|
is_typed = False
|
||
|
elif fullname in TYPED_NAMEDTUPLE_NAMES:
|
||
|
is_typed = True
|
||
|
else:
|
||
|
return None, None, []
|
||
|
result = self.parse_namedtuple_args(call, fullname)
|
||
|
if result:
|
||
|
items, types, defaults, typename, tvar_defs, ok = result
|
||
|
else:
|
||
|
# Error. Construct dummy return value.
|
||
|
if var_name:
|
||
|
name = var_name
|
||
|
if is_func_scope:
|
||
|
name += "@" + str(call.line)
|
||
|
else:
|
||
|
name = var_name = "namedtuple@" + str(call.line)
|
||
|
info = self.build_namedtuple_typeinfo(name, [], [], {}, node.line, None)
|
||
|
self.store_namedtuple_info(info, var_name, call, is_typed)
|
||
|
if name != var_name or is_func_scope:
|
||
|
# NOTE: we skip local namespaces since they are not serialized.
|
||
|
self.api.add_symbol_skip_local(name, info)
|
||
|
return var_name, info, []
|
||
|
if not ok:
|
||
|
# This is a valid named tuple but some types are not ready.
|
||
|
return typename, None, []
|
||
|
|
||
|
# We use the variable name as the class name if it exists. If
|
||
|
# it doesn't, we use the name passed as an argument. We prefer
|
||
|
# the variable name because it should be unique inside a
|
||
|
# module, and so we don't need to disambiguate it with a line
|
||
|
# number.
|
||
|
if var_name:
|
||
|
name = var_name
|
||
|
else:
|
||
|
name = typename
|
||
|
|
||
|
if var_name is None or is_func_scope:
|
||
|
# There are two special cases where need to give it a unique name derived
|
||
|
# from the line number:
|
||
|
# * This is a base class expression, since it often matches the class name:
|
||
|
# class NT(NamedTuple('NT', [...])):
|
||
|
# ...
|
||
|
# * This is a local (function or method level) named tuple, since
|
||
|
# two methods of a class can define a named tuple with the same name,
|
||
|
# and they will be stored in the same namespace (see below).
|
||
|
name += "@" + str(call.line)
|
||
|
if defaults:
|
||
|
default_items = {
|
||
|
arg_name: default for arg_name, default in zip(items[-len(defaults) :], defaults)
|
||
|
}
|
||
|
else:
|
||
|
default_items = {}
|
||
|
|
||
|
existing_info = None
|
||
|
if isinstance(node.analyzed, NamedTupleExpr):
|
||
|
existing_info = node.analyzed.info
|
||
|
info = self.build_namedtuple_typeinfo(
|
||
|
name, items, types, default_items, node.line, existing_info
|
||
|
)
|
||
|
|
||
|
# If var_name is not None (i.e. this is not a base class expression), we always
|
||
|
# store the generated TypeInfo under var_name in the current scope, so that
|
||
|
# other definitions can use it.
|
||
|
if var_name:
|
||
|
self.store_namedtuple_info(info, var_name, call, is_typed)
|
||
|
else:
|
||
|
call.analyzed = NamedTupleExpr(info, is_typed=is_typed)
|
||
|
call.analyzed.set_line(call)
|
||
|
# There are three cases where we need to store the generated TypeInfo
|
||
|
# second time (for the purpose of serialization):
|
||
|
# * If there is a name mismatch like One = NamedTuple('Other', [...])
|
||
|
# we also store the info under name 'Other@lineno', this is needed
|
||
|
# because classes are (de)serialized using their actual fullname, not
|
||
|
# the name of l.h.s.
|
||
|
# * If this is a method level named tuple. It can leak from the method
|
||
|
# via assignment to self attribute and therefore needs to be serialized
|
||
|
# (local namespaces are not serialized).
|
||
|
# * If it is a base class expression. It was not stored above, since
|
||
|
# there is no var_name (but it still needs to be serialized
|
||
|
# since it is in MRO of some class).
|
||
|
if name != var_name or is_func_scope:
|
||
|
# NOTE: we skip local namespaces since they are not serialized.
|
||
|
self.api.add_symbol_skip_local(name, info)
|
||
|
return typename, info, tvar_defs
|
||
|
|
||
|
def store_namedtuple_info(
|
||
|
self, info: TypeInfo, name: str, call: CallExpr, is_typed: bool
|
||
|
) -> None:
|
||
|
self.api.add_symbol(name, info, call)
|
||
|
call.analyzed = NamedTupleExpr(info, is_typed=is_typed)
|
||
|
call.analyzed.set_line(call)
|
||
|
|
||
|
def parse_namedtuple_args(
|
||
|
self, call: CallExpr, fullname: str
|
||
|
) -> None | (tuple[list[str], list[Type], list[Expression], str, list[TypeVarLikeType], bool]):
|
||
|
"""Parse a namedtuple() call into data needed to construct a type.
|
||
|
|
||
|
Returns a 6-tuple:
|
||
|
- List of argument names
|
||
|
- List of argument types
|
||
|
- List of default values
|
||
|
- First argument of namedtuple
|
||
|
- All typevars found in the field definition
|
||
|
- Whether all types are ready.
|
||
|
|
||
|
Return None if the definition didn't typecheck.
|
||
|
"""
|
||
|
type_name = "NamedTuple" if fullname in TYPED_NAMEDTUPLE_NAMES else "namedtuple"
|
||
|
# TODO: Share code with check_argument_count in checkexpr.py?
|
||
|
args = call.args
|
||
|
if len(args) < 2:
|
||
|
self.fail(f'Too few arguments for "{type_name}()"', call)
|
||
|
return None
|
||
|
defaults: list[Expression] = []
|
||
|
if len(args) > 2:
|
||
|
# Typed namedtuple doesn't support additional arguments.
|
||
|
if fullname in TYPED_NAMEDTUPLE_NAMES:
|
||
|
self.fail('Too many arguments for "NamedTuple()"', call)
|
||
|
return None
|
||
|
for i, arg_name in enumerate(call.arg_names[2:], 2):
|
||
|
if arg_name == "defaults":
|
||
|
arg = args[i]
|
||
|
# We don't care what the values are, as long as the argument is an iterable
|
||
|
# and we can count how many defaults there are.
|
||
|
if isinstance(arg, (ListExpr, TupleExpr)):
|
||
|
defaults = list(arg.items)
|
||
|
else:
|
||
|
self.fail(
|
||
|
"List or tuple literal expected as the defaults argument to "
|
||
|
"{}()".format(type_name),
|
||
|
arg,
|
||
|
)
|
||
|
break
|
||
|
if call.arg_kinds[:2] != [ARG_POS, ARG_POS]:
|
||
|
self.fail(f'Unexpected arguments to "{type_name}()"', call)
|
||
|
return None
|
||
|
if not isinstance(args[0], StrExpr):
|
||
|
self.fail(f'"{type_name}()" expects a string literal as the first argument', call)
|
||
|
return None
|
||
|
typename = args[0].value
|
||
|
types: list[Type] = []
|
||
|
tvar_defs = []
|
||
|
if not isinstance(args[1], (ListExpr, TupleExpr)):
|
||
|
if fullname == "collections.namedtuple" and isinstance(args[1], StrExpr):
|
||
|
str_expr = args[1]
|
||
|
items = str_expr.value.replace(",", " ").split()
|
||
|
else:
|
||
|
self.fail(
|
||
|
'List or tuple literal expected as the second argument to "{}()"'.format(
|
||
|
type_name
|
||
|
),
|
||
|
call,
|
||
|
)
|
||
|
return None
|
||
|
else:
|
||
|
listexpr = args[1]
|
||
|
if fullname == "collections.namedtuple":
|
||
|
# The fields argument contains just names, with implicit Any types.
|
||
|
if not is_StrExpr_list(listexpr.items):
|
||
|
self.fail('String literal expected as "namedtuple()" item', call)
|
||
|
return None
|
||
|
items = [item.value for item in listexpr.items]
|
||
|
else:
|
||
|
type_exprs = [
|
||
|
t.items[1]
|
||
|
for t in listexpr.items
|
||
|
if isinstance(t, TupleExpr) and len(t.items) == 2
|
||
|
]
|
||
|
tvar_defs = self.api.get_and_bind_all_tvars(type_exprs)
|
||
|
# The fields argument contains (name, type) tuples.
|
||
|
result = self.parse_namedtuple_fields_with_types(listexpr.items, call)
|
||
|
if result is None:
|
||
|
# One of the types is not ready, defer.
|
||
|
return None
|
||
|
items, types, _, ok = result
|
||
|
if not ok:
|
||
|
return [], [], [], typename, [], False
|
||
|
if not types:
|
||
|
types = [AnyType(TypeOfAny.unannotated) for _ in items]
|
||
|
underscore = [item for item in items if item.startswith("_")]
|
||
|
if underscore:
|
||
|
self.fail(
|
||
|
f'"{type_name}()" field names cannot start with an underscore: '
|
||
|
+ ", ".join(underscore),
|
||
|
call,
|
||
|
)
|
||
|
if len(defaults) > len(items):
|
||
|
self.fail(f'Too many defaults given in call to "{type_name}()"', call)
|
||
|
defaults = defaults[: len(items)]
|
||
|
return items, types, defaults, typename, tvar_defs, True
|
||
|
|
||
|
def parse_namedtuple_fields_with_types(
|
||
|
self, nodes: list[Expression], context: Context
|
||
|
) -> tuple[list[str], list[Type], list[Expression], bool] | None:
|
||
|
"""Parse typed named tuple fields.
|
||
|
|
||
|
Return (names, types, defaults, whether types are all ready), or None if error occurred.
|
||
|
"""
|
||
|
items: list[str] = []
|
||
|
types: list[Type] = []
|
||
|
for item in nodes:
|
||
|
if isinstance(item, TupleExpr):
|
||
|
if len(item.items) != 2:
|
||
|
self.fail('Invalid "NamedTuple()" field definition', item)
|
||
|
return None
|
||
|
name, type_node = item.items
|
||
|
if isinstance(name, StrExpr):
|
||
|
items.append(name.value)
|
||
|
else:
|
||
|
self.fail('Invalid "NamedTuple()" field name', item)
|
||
|
return None
|
||
|
try:
|
||
|
type = expr_to_unanalyzed_type(type_node, self.options, self.api.is_stub_file)
|
||
|
except TypeTranslationError:
|
||
|
self.fail("Invalid field type", type_node)
|
||
|
return None
|
||
|
# We never allow recursive types at function scope.
|
||
|
analyzed = self.api.anal_type(
|
||
|
type,
|
||
|
allow_placeholder=not self.options.disable_recursive_aliases
|
||
|
and not self.api.is_func_scope(),
|
||
|
prohibit_self_type="NamedTuple item type",
|
||
|
)
|
||
|
# Workaround #4987 and avoid introducing a bogus UnboundType
|
||
|
if isinstance(analyzed, UnboundType):
|
||
|
analyzed = AnyType(TypeOfAny.from_error)
|
||
|
# These should be all known, otherwise we would defer in visit_assignment_stmt().
|
||
|
if analyzed is None:
|
||
|
return [], [], [], False
|
||
|
types.append(analyzed)
|
||
|
else:
|
||
|
self.fail('Tuple expected as "NamedTuple()" field', item)
|
||
|
return None
|
||
|
return items, types, [], True
|
||
|
|
||
|
def build_namedtuple_typeinfo(
|
||
|
self,
|
||
|
name: str,
|
||
|
items: list[str],
|
||
|
types: list[Type],
|
||
|
default_items: Mapping[str, Expression],
|
||
|
line: int,
|
||
|
existing_info: TypeInfo | None,
|
||
|
) -> TypeInfo:
|
||
|
strtype = self.api.named_type("builtins.str")
|
||
|
implicit_any = AnyType(TypeOfAny.special_form)
|
||
|
basetuple_type = self.api.named_type("builtins.tuple", [implicit_any])
|
||
|
dictype = self.api.named_type("builtins.dict", [strtype, implicit_any])
|
||
|
# Actual signature should return OrderedDict[str, Union[types]]
|
||
|
ordereddictype = self.api.named_type("builtins.dict", [strtype, implicit_any])
|
||
|
fallback = self.api.named_type("builtins.tuple", [implicit_any])
|
||
|
# Note: actual signature should accept an invariant version of Iterable[UnionType[types]].
|
||
|
# but it can't be expressed. 'new' and 'len' should be callable types.
|
||
|
iterable_type = self.api.named_type_or_none("typing.Iterable", [implicit_any])
|
||
|
function_type = self.api.named_type("builtins.function")
|
||
|
|
||
|
literals: list[Type] = [LiteralType(item, strtype) for item in items]
|
||
|
match_args_type = TupleType(literals, basetuple_type)
|
||
|
|
||
|
info = existing_info or self.api.basic_new_typeinfo(name, fallback, line)
|
||
|
info.is_named_tuple = True
|
||
|
tuple_base = TupleType(types, fallback)
|
||
|
if info.special_alias and has_placeholder(info.special_alias.target):
|
||
|
self.api.process_placeholder(
|
||
|
None, "NamedTuple item", info, force_progress=tuple_base != info.tuple_type
|
||
|
)
|
||
|
info.update_tuple_type(tuple_base)
|
||
|
info.line = line
|
||
|
# For use by mypyc.
|
||
|
info.metadata["namedtuple"] = {"fields": items.copy()}
|
||
|
|
||
|
# We can't calculate the complete fallback type until after semantic
|
||
|
# analysis, since otherwise base classes might be incomplete. Postpone a
|
||
|
# callback function that patches the fallback.
|
||
|
if not has_placeholder(tuple_base) and not has_type_vars(tuple_base):
|
||
|
self.api.schedule_patch(
|
||
|
PRIORITY_FALLBACKS, lambda: calculate_tuple_fallback(tuple_base)
|
||
|
)
|
||
|
|
||
|
def add_field(
|
||
|
var: Var, is_initialized_in_class: bool = False, is_property: bool = False
|
||
|
) -> None:
|
||
|
var.info = info
|
||
|
var.is_initialized_in_class = is_initialized_in_class
|
||
|
var.is_property = is_property
|
||
|
var._fullname = f"{info.fullname}.{var.name}"
|
||
|
info.names[var.name] = SymbolTableNode(MDEF, var)
|
||
|
|
||
|
fields = [Var(item, typ) for item, typ in zip(items, types)]
|
||
|
for var in fields:
|
||
|
add_field(var, is_property=True)
|
||
|
# We can't share Vars between fields and method arguments, since they
|
||
|
# have different full names (the latter are normally used as local variables
|
||
|
# in functions, so their full names are set to short names when generated methods
|
||
|
# are analyzed).
|
||
|
vars = [Var(item, typ) for item, typ in zip(items, types)]
|
||
|
|
||
|
tuple_of_strings = TupleType([strtype for _ in items], basetuple_type)
|
||
|
add_field(Var("_fields", tuple_of_strings), is_initialized_in_class=True)
|
||
|
add_field(Var("_field_types", dictype), is_initialized_in_class=True)
|
||
|
add_field(Var("_field_defaults", dictype), is_initialized_in_class=True)
|
||
|
add_field(Var("_source", strtype), is_initialized_in_class=True)
|
||
|
add_field(Var("__annotations__", ordereddictype), is_initialized_in_class=True)
|
||
|
add_field(Var("__doc__", strtype), is_initialized_in_class=True)
|
||
|
if self.options.python_version >= (3, 10):
|
||
|
add_field(Var("__match_args__", match_args_type), is_initialized_in_class=True)
|
||
|
|
||
|
assert info.tuple_type is not None # Set by update_tuple_type() above.
|
||
|
tvd = TypeVarType(
|
||
|
name=SELF_TVAR_NAME,
|
||
|
fullname=info.fullname + "." + SELF_TVAR_NAME,
|
||
|
id=self.api.tvar_scope.new_unique_func_id(),
|
||
|
values=[],
|
||
|
upper_bound=info.tuple_type,
|
||
|
default=AnyType(TypeOfAny.from_omitted_generics),
|
||
|
)
|
||
|
selftype = tvd
|
||
|
|
||
|
def add_method(
|
||
|
funcname: str,
|
||
|
ret: Type,
|
||
|
args: list[Argument],
|
||
|
is_classmethod: bool = False,
|
||
|
is_new: bool = False,
|
||
|
) -> None:
|
||
|
if is_classmethod or is_new:
|
||
|
first = [Argument(Var("_cls"), TypeType.make_normalized(selftype), None, ARG_POS)]
|
||
|
else:
|
||
|
first = [Argument(Var("_self"), selftype, None, ARG_POS)]
|
||
|
args = first + args
|
||
|
|
||
|
types = [arg.type_annotation for arg in args]
|
||
|
items = [arg.variable.name for arg in args]
|
||
|
arg_kinds = [arg.kind for arg in args]
|
||
|
assert None not in types
|
||
|
signature = CallableType(cast(List[Type], types), arg_kinds, items, ret, function_type)
|
||
|
signature.variables = [tvd]
|
||
|
func = FuncDef(funcname, args, Block([]))
|
||
|
func.info = info
|
||
|
func.is_class = is_classmethod
|
||
|
func.type = set_callable_name(signature, func)
|
||
|
func._fullname = info.fullname + "." + funcname
|
||
|
func.line = line
|
||
|
if is_classmethod:
|
||
|
v = Var(funcname, func.type)
|
||
|
v.is_classmethod = True
|
||
|
v.info = info
|
||
|
v._fullname = func._fullname
|
||
|
func.is_decorated = True
|
||
|
dec = Decorator(func, [NameExpr("classmethod")], v)
|
||
|
dec.line = line
|
||
|
sym = SymbolTableNode(MDEF, dec)
|
||
|
else:
|
||
|
sym = SymbolTableNode(MDEF, func)
|
||
|
sym.plugin_generated = True
|
||
|
info.names[funcname] = sym
|
||
|
|
||
|
add_method(
|
||
|
"_replace",
|
||
|
ret=selftype,
|
||
|
args=[Argument(var, var.type, EllipsisExpr(), ARG_NAMED_OPT) for var in vars],
|
||
|
)
|
||
|
|
||
|
def make_init_arg(var: Var) -> Argument:
|
||
|
default = default_items.get(var.name, None)
|
||
|
kind = ARG_POS if default is None else ARG_OPT
|
||
|
return Argument(var, var.type, default, kind)
|
||
|
|
||
|
add_method("__new__", ret=selftype, args=[make_init_arg(var) for var in vars], is_new=True)
|
||
|
add_method("_asdict", args=[], ret=ordereddictype)
|
||
|
add_method(
|
||
|
"_make",
|
||
|
ret=selftype,
|
||
|
is_classmethod=True,
|
||
|
args=[Argument(Var("iterable", iterable_type), iterable_type, None, ARG_POS)],
|
||
|
)
|
||
|
|
||
|
self_tvar_expr = TypeVarExpr(
|
||
|
SELF_TVAR_NAME,
|
||
|
info.fullname + "." + SELF_TVAR_NAME,
|
||
|
[],
|
||
|
info.tuple_type,
|
||
|
AnyType(TypeOfAny.from_omitted_generics),
|
||
|
)
|
||
|
info.names[SELF_TVAR_NAME] = SymbolTableNode(MDEF, self_tvar_expr)
|
||
|
return info
|
||
|
|
||
|
@contextmanager
|
||
|
def save_namedtuple_body(self, named_tuple_info: TypeInfo) -> Iterator[None]:
|
||
|
"""Preserve the generated body of class-based named tuple and then restore it.
|
||
|
|
||
|
Temporarily clear the names dict so we don't get errors about duplicate names
|
||
|
that were already set in build_namedtuple_typeinfo (we already added the tuple
|
||
|
field names while generating the TypeInfo, and actual duplicates are
|
||
|
already reported).
|
||
|
"""
|
||
|
nt_names = named_tuple_info.names
|
||
|
named_tuple_info.names = SymbolTable()
|
||
|
|
||
|
yield
|
||
|
|
||
|
# Make sure we didn't use illegal names, then reset the names in the typeinfo.
|
||
|
for prohibited in NAMEDTUPLE_PROHIBITED_NAMES:
|
||
|
if prohibited in named_tuple_info.names:
|
||
|
if nt_names.get(prohibited) is named_tuple_info.names[prohibited]:
|
||
|
continue
|
||
|
ctx = named_tuple_info.names[prohibited].node
|
||
|
assert ctx is not None
|
||
|
self.fail(f'Cannot overwrite NamedTuple attribute "{prohibited}"', ctx)
|
||
|
|
||
|
# Restore the names in the original symbol table. This ensures that the symbol
|
||
|
# table contains the field objects created by build_namedtuple_typeinfo. Exclude
|
||
|
# __doc__, which can legally be overwritten by the class.
|
||
|
for key, value in nt_names.items():
|
||
|
if key in named_tuple_info.names:
|
||
|
if key == "__doc__":
|
||
|
continue
|
||
|
sym = named_tuple_info.names[key]
|
||
|
if isinstance(sym.node, (FuncBase, Decorator)) and not sym.plugin_generated:
|
||
|
# Keep user-defined methods as is.
|
||
|
continue
|
||
|
# Keep existing (user-provided) definitions under mangled names, so they
|
||
|
# get semantically analyzed.
|
||
|
r_key = get_unique_redefinition_name(key, named_tuple_info.names)
|
||
|
named_tuple_info.names[r_key] = sym
|
||
|
named_tuple_info.names[key] = value
|
||
|
|
||
|
# Helpers
|
||
|
|
||
|
def fail(self, msg: str, ctx: Context) -> None:
|
||
|
self.api.fail(msg, ctx)
|