Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Try another approach
  • Loading branch information
ilevkivskyi committed Aug 9, 2022
commit f0163c433c546d4e406539e8c9889c5a6917de2f
6 changes: 6 additions & 0 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,12 @@ def lookup_fully_qualified_alias(
node = stnode.node if stnode else None
if isinstance(node, TypeAlias):
return node
elif isinstance(node, TypeInfo):
assert node.tuple_type
[]))
alias = TypeAlias(target, node.fullname, node.line, node.column)
node.tuple_alias = alias
return alias
else:
# Looks like a missing TypeAlias during an initial daemon load, put something there
assert (
Expand Down
5 changes: 5 additions & 0 deletions mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -2292,6 +2292,11 @@ def visit_instance(self, t: Instance) -> None:
self.instances.append(t)
super().visit_instance(t)

def visit_type_alias_type(self, t: TypeAliasType) -> None:
if t.alias and not t.is_recursive:
t.alias.target.accept(self)
super().visit_type_alias_type(t)


def find_type_overlaps(*types: Type) -> Set[str]:
"""Return a set of fullnames that share a short name and appear in either type.
Expand Down
5 changes: 5 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2651,6 +2651,7 @@ class is generic then it will be a type constructor of higher kind.
"bases",
"_promote",
"tuple_type",
"tuple_alias",
"is_named_tuple",
"typeddict_type",
"is_newtype",
Expand Down Expand Up @@ -2789,6 +2790,9 @@ class is generic then it will be a type constructor of higher kind.
# It is useful for plugins to add their data to save in the cache.
metadata: Dict[str, JsonDict]

# Store type alias representing this type (for named tuples).
tuple_alias: Optional["TypeAlias"]

FLAGS: Final = [
"is_abstract",
"is_enum",
Expand Down Expand Up @@ -2835,6 +2839,7 @@ def __init__(self, names: "SymbolTable", defn: ClassDef, module_name: str) -> No
self._promote = []
self.alt_promote = None
self.tuple_type = None
self.tuple_alias = None
self.is_named_tuple = False
self.typeddict_type = None
self.is_newtype = False
Expand Down
49 changes: 22 additions & 27 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,11 @@
PRIORITY_FALLBACKS,
SemanticAnalyzerInterface,
calculate_tuple_fallback,
has_placeholder,
set_callable_name as set_callable_name,
)
from mypy.semanal_typeddict import TypedDictAnalyzer
from mypy.tvar_scope import TypeVarLikeScope
from mypy.type_visitor import TypeQuery
from mypy.typeanal import (
TypeAnalyser,
TypeVarLikeList,
Expand Down Expand Up @@ -1424,7 +1424,12 @@ def analyze_class_body_common(self, defn: ClassDef) -> None:

def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool:
"""Check if this class can define a named tuple."""
if defn.info and defn.info.is_named_tuple:
if (
defn.info
and defn.info.is_named_tuple
and defn.info.tuple_type
and not has_placeholder(defn.info.tuple_type)
):
# Don't reprocess everything. We just need to process methods defined
# in the named tuple class body.
is_named_tuple, info = True, defn.info # type: bool, Optional[TypeInfo]
Expand All @@ -1436,6 +1441,8 @@ def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool:
if info is None:
self.mark_incomplete(defn.name, defn)
else:
if info.tuple_type and has_placeholder(info.tuple_type):
self.defer(force_progress=True)
self.prepare_class_def(defn, info)
with self.scope.class_scope(defn.info):
with self.named_tuple_analyzer.save_namedtuple_body(info):
Expand Down Expand Up @@ -1784,7 +1791,7 @@ def configure_base_classes(
info.tuple_type = None
for base, base_expr in bases:
if isinstance(base, TupleType):
actual_base = self.configure_tuple_base_class(defn, base, base_expr)
actual_base = self.configure_tuple_base_class(defn, base)
base_types.append(actual_base)
elif isinstance(base, Instance):
if base.type.is_newtype:
Expand Down Expand Up @@ -1827,9 +1834,7 @@ def configure_base_classes(
return
self.calculate_class_mro(defn, self.object_type)

def configure_tuple_base_class(
self, defn: ClassDef, base: TupleType, base_expr: Expression
) -> Instance:
def configure_tuple_base_class(self, defn: ClassDef, base: TupleType) -> Instance:
info = defn.info

# There may be an existing valid tuple type from previous semanal iterations.
Expand All @@ -1838,10 +1843,6 @@ def configure_tuple_base_class(
self.fail("Class has two incompatible bases derived from tuple", defn)
defn.has_incompatible_baseclass = True
info.tuple_type = base
if isinstance(base_expr, CallExpr):
defn.analyzed = NamedTupleExpr(base.partial_fallback.type)
defn.analyzed.line = defn.line
defn.analyzed.column = defn.column

if base.partial_fallback.type.fullname == "builtins.tuple":
# Fallback can only be safely calculated after semantic analysis, since base
Expand Down Expand Up @@ -2626,7 +2627,10 @@ def analyze_enum_assign(self, s: AssignmentStmt) -> bool:
def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
"""Check if s defines a namedtuple."""
if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.analyzed, NamedTupleExpr):
return True # This is a valid and analyzed named tuple definition, nothing to do here.
if s.rvalue.analyzed.info.tuple_type and not has_placeholder(
s.rvalue.analyzed.info.tuple_type
):
return True # This is a valid and analyzed named tuple definition, nothing to do here.
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], (NameExpr, MemberExpr)):
return False
lvalue = s.lvalues[0]
Expand Down Expand Up @@ -3025,6 +3029,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
# unless using PEP 613 `cls: TypeAlias = A`
return False

if isinstance(s.rvalue, CallExpr) and s.rvalue.analyzed:
return False

existing = self.current_symbol_table().get(lvalue.name)
# Third rule: type aliases can't be re-defined. For example:
# A: Type[float] = int
Expand Down Expand Up @@ -3157,9 +3164,8 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool:
self.cannot_resolve_name(lvalue.name, "name", s)
return True
else:
self.progress = True
# We need to defer so that this change can get propagated to base classes.
self.defer(s)
self.defer(s, force_progress=True)
else:
self.add_symbol(lvalue.name, alias_node, s)
if isinstance(rvalue, RefExpr) and isinstance(rvalue.node, TypeAlias):
Expand Down Expand Up @@ -5464,7 +5470,7 @@ def tvar_scope_frame(self, frame: TypeVarLikeScope) -> Iterator[None]:
yield
self.tvar_scope = old_scope

def defer(self, debug_context: Optional[Context] = None) -> None:
def defer(self, debug_context: Optional[Context] = None, force_progress: bool = False) -> None:
"""Defer current analysis target to be analyzed again.

This must be called if something in the current target is
Expand All @@ -5478,6 +5484,8 @@ def defer(self, debug_context: Optional[Context] = None) -> None:
They are usually preferable to a direct defer() call.
"""
assert not self.final_iteration, "Must not defer during final iteration"
if force_progress:
self.progress = True
self.deferred = True
# Store debug info for this deferral.
line = (
Expand Down Expand Up @@ -5979,19 +5987,6 @@ def is_future_flag_set(self, flag: str) -> bool:
return self.modules[self.cur_mod_id].is_future_flag_set(flag)


class HasPlaceholders(TypeQuery[bool]):
def __init__(self) -> None:
super().__init__(any)

def visit_placeholder_type(self, t: PlaceholderType) -> bool:
return True


def has_placeholder(typ: Type) -> bool:
"""Check if a type contains any placeholder types (recursively)."""
return typ.accept(HasPlaceholders())


def replace_implicit_first_type(sig: FunctionLike, new: Type) -> FunctionLike:
if isinstance(sig, CallableType):
if len(sig.arg_types) == 0:
Expand Down
49 changes: 42 additions & 7 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
SymbolTableNode,
TempNode,
TupleExpr,
TypeAlias,
TypeInfo,
TypeVarExpr,
Var,
Expand All @@ -44,12 +45,14 @@
PRIORITY_FALLBACKS,
SemanticAnalyzerInterface,
calculate_tuple_fallback,
has_placeholder,
set_callable_name,
)
from mypy.types import (
TYPED_NAMEDTUPLE_NAMES,
AnyType,
CallableType,
Instance,
LiteralType,
TupleType,
Type,
Expand Down Expand Up @@ -109,8 +112,11 @@ def analyze_namedtuple_classdef(
items, types, default_items = 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
defn.name, items, types, default_items, defn.line, existing_info
)
defn.info = info
defn.analyzed = NamedTupleExpr(info, is_typed=True)
Expand Down Expand Up @@ -164,7 +170,9 @@ def check_namedtuple_classdef(
if stmt.type is None:
types.append(AnyType(TypeOfAny.unannotated))
else:
analyzed = self.api.anal_type(stmt.type)
analyzed = self.api.anal_type(
stmt.type, allow_placeholder=self.options.enable_recursive_aliases
)
if analyzed is None:
# Something is incomplete. We need to defer this named tuple.
return None
Expand Down Expand Up @@ -226,7 +234,7 @@ def check_namedtuple(
name += "@" + str(call.line)
else:
name = var_name = "namedtuple@" + str(call.line)
info = self.build_namedtuple_typeinfo(name, [], [], {}, node.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.
Expand Down Expand Up @@ -262,12 +270,28 @@ def check_namedtuple(
}
else:
default_items = {}
info = self.build_namedtuple_typeinfo(name, items, types, default_items, node.line)

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 isinstance(node.analyzed, NamedTupleExpr) and node.analyzed.info.tuple_type:
if has_placeholder(node.analyzed.info.tuple_type):
self.api.defer(force_progress=True)
# No need to store in symbol tables.
return typename, 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', [...])
Expand Down Expand Up @@ -408,7 +432,9 @@ def parse_namedtuple_fields_with_types(
except TypeTranslationError:
self.fail("Invalid field type", type_node)
return None
analyzed = self.api.anal_type(type)
analyzed = self.api.anal_type(
type, allow_placeholder=self.options.enable_recursive_aliases
)
# Workaround #4987 and avoid introducing a bogus UnboundType
if isinstance(analyzed, UnboundType):
analyzed = AnyType(TypeOfAny.from_error)
Expand All @@ -428,6 +454,7 @@ def build_namedtuple_typeinfo(
types: List[Type],
default_items: Mapping[str, Expression],
line: int,
existing_info: Optional[TypeInfo],
) -> TypeInfo:
strtype = self.api.named_type("builtins.str")
implicit_any = AnyType(TypeOfAny.special_form)
Expand All @@ -448,18 +475,26 @@ def build_namedtuple_typeinfo(
literals: List[Type] = [LiteralType(item, strtype) for item in items]
match_args_type = TupleType(literals, basetuple_type)

info = self.api.basic_new_typeinfo(name, fallback, line)
info = existing_info or self.api.basic_new_typeinfo(name, fallback, line)
info.is_named_tuple = True
tuple_base = TupleType(types, fallback)
info.tuple_type = tuple_base
[]))
if not info.tuple_alias:
info.tuple_alias = TypeAlias(target, info.fullname, info.line, info.column)
else:
info.tuple_alias.target = target
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.
self.api.schedule_patch(PRIORITY_FALLBACKS, lambda: calculate_tuple_fallback(tuple_base))
if not has_placeholder(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
Expand Down
18 changes: 17 additions & 1 deletion mypy/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
TypeInfo,
)
from mypy.tvar_scope import TypeVarLikeScope
from mypy.type_visitor import TypeQuery
from mypy.types import (
TPDICT_FB_NAMES,
FunctionLike,
Instance,
Parameters,
ParamSpecFlavor,
ParamSpecType,
PlaceholderType,
ProperType,
TupleType,
Type,
Expand Down Expand Up @@ -82,7 +84,7 @@ def record_incomplete_ref(self) -> None:
raise NotImplementedError

@abstractmethod
def defer(self) -> None:
def defer(self, debug_context: Optional[Context] = ..., force_progress: bool = ...) -> None:
raise NotImplementedError

@abstractmethod
Expand Down Expand Up @@ -147,6 +149,7 @@ def anal_type(
allow_tuple_literal: bool = False,
allow_unbound_tvars: bool = False,
allow_required: bool = False,
allow_placeholder: bool = False,
report_invalid_types: bool = True,
) -> Optional[Type]:
raise NotImplementedError
Expand Down Expand Up @@ -297,3 +300,16 @@ def paramspec_kwargs(
column=column,
prefix=prefix,
)


class HasPlaceholders(TypeQuery[bool]):
def __init__(self) -> None:
super().__init__(any)

def visit_placeholder_type(self, t: PlaceholderType) -> bool:
return True


def has_placeholder(typ: Type) -> bool:
"""Check if a type contains any placeholder types (recursively)."""
return typ.accept(HasPlaceholders())
2 changes: 2 additions & 0 deletions mypy/server/astmerge.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,8 @@ def process_type_info(self, info: Optional[TypeInfo]) -> None:
self.fixup_type(target)
self.fixup_type(info.tuple_type)
self.fixup_type(info.typeddict_type)
if info.tuple_alias:
self.fixup_type(info.tuple_alias.target)
info.defn.info = self.fixup(info)
replace_nodes_in_symbol_table(info.names, self.replacements)
for i, item in enumerate(info.mro):
Expand Down
2 changes: 2 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,8 @@ def analyze_type_with_type_info(
if args:
self.fail("Generic tuple types not supported", ctx)
return AnyType(TypeOfAny.from_error)
if info.tuple_alias:
return TypeAliasType(info.tuple_alias, [])
return tup.copy_modified(items=self.anal_array(tup.items), fallback=instance)
td = info.typeddict_type
if td is not None:
Expand Down
Loading