Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
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
5 changes: 3 additions & 2 deletions mypy/checkmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
The return type of the appropriate ``__get__`` overload for the descriptor.
"""
instance_type = get_proper_type(mx.original_type)
orig_descriptor_type = descriptor_type
descriptor_type = get_proper_type(descriptor_type)

if isinstance(descriptor_type, UnionType):
Expand All @@ -571,10 +572,10 @@ def analyze_descriptor_access(descriptor_type: Type, mx: MemberContext) -> Type:
[analyze_descriptor_access(typ, mx) for typ in descriptor_type.items]
)
elif not isinstance(descriptor_type, Instance):
return descriptor_type
return orig_descriptor_type

if not descriptor_type.type.has_readable_member("__get__"):
return descriptor_type
return orig_descriptor_type

dunder_get = descriptor_type.type.get_method("__get__")
if dunder_get is None:
Expand Down
10 changes: 10 additions & 0 deletions mypy/fixup.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,16 @@ def lookup_fully_qualified_alias(
node = stnode.node if stnode else None
if isinstance(node, TypeAlias):
return node
elif isinstance(node, TypeInfo):
if node.tuple_type:
alias = TypeAlias.from_tuple_type(node)
elif node.typeddict_type:
alias = TypeAlias.from_typeddict_type(node)
else:
assert allow_missing
return missing_alias()
node.special_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
53 changes: 53 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2656,6 +2656,7 @@ class is generic then it will be a type constructor of higher kind.
"bases",
"_promote",
"tuple_type",
"special_alias",
"is_named_tuple",
"typeddict_type",
"is_newtype",
Expand Down Expand Up @@ -2794,6 +2795,17 @@ 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 and TypedDicts).
# Although definitions of these types are stored in symbol tables as TypeInfo,
# when a type analyzer will find them, it should construct a TupleType, or
# a TypedDict type. However, we can't use the plain types, since if the definition
# is recursive, this will create an actual recursive structure of types (i.e. as
# internal Python objects) causing infinite recursions everywhere during type checking.
# To overcome this, we create a TypeAlias node, that will point to these types.
# We store this node in the `special_alias` attribute, because it must be the same node
# in case we are doing multiple semantic analysis passes.
special_alias: Optional["TypeAlias"]

FLAGS: Final = [
"is_abstract",
"is_enum",
Expand Down Expand Up @@ -2840,6 +2852,7 @@ def __init__(self, names: "SymbolTable", defn: ClassDef, module_name: str) -> No
self._promote = []
self.alt_promote = None
self.tuple_type = None
self.special_alias = None
self.is_named_tuple = False
self.typeddict_type = None
self.is_newtype = False
Expand Down Expand Up @@ -2970,6 +2983,24 @@ def direct_base_classes(self) -> "List[TypeInfo]":
"""
return [base.type for base in self.bases]

def update_tuple_type(self, typ: "mypy.types.TupleType") -> None:
"""Update tuple_type and special_alias as needed."""
self.tuple_type = typ
alias = TypeAlias.from_tuple_type(self)
if not self.special_alias:
self.special_alias = alias
else:
self.special_alias.target = alias.target

def update_typeddict_type(self, typ: "mypy.types.TypedDictType") -> None:
"""Update typeddict_type and special_alias as needed."""
self.typeddict_type = typ
alias = TypeAlias.from_typeddict_type(self)
if not self.special_alias:
self.special_alias = alias
else:
self.special_alias.target = alias.target

def __str__(self) -> str:
"""Return a string representation of the type.

Expand Down Expand Up @@ -3258,6 +3289,28 @@ def __init__(
self.eager = eager
super().__init__(line, column)

@classmethod
def from_tuple_type(cls, info: TypeInfo) -> "TypeAlias":
"""Generate an alias to the tuple type described by a given TypeInfo."""
assert info.tuple_type
return TypeAlias(
info.tuple_type.copy_modified(fallback=mypy.types.Instance(info, [])),
info.fullname,
info.line,
info.column,
)

@classmethod
def from_typeddict_type(cls, info: TypeInfo) -> "TypeAlias":
"""Generate an alias to the TypedDict type described by a given TypeInfo."""
assert info.typeddict_type
return TypeAlias(
info.typeddict_type.copy_modified(fallback=mypy.types.Instance(info, [])),
info.fullname,
info.line,
info.column,
)

@property
def name(self) -> str:
return self._fullname.split(".")[-1]
Expand Down
97 changes: 57 additions & 40 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 @@ -1378,17 +1378,7 @@ def analyze_class(self, defn: ClassDef) -> None:
self.mark_incomplete(defn.name, defn)
return

is_typeddict, info = self.typed_dict_analyzer.analyze_typeddict_classdef(defn)
if is_typeddict:
for decorator in defn.decorators:
decorator.accept(self)
if isinstance(decorator, RefExpr):
if decorator.fullname in FINAL_DECORATOR_NAMES:
self.fail("@final cannot be used with TypedDict", decorator)
if info is None:
self.mark_incomplete(defn.name, defn)
else:
self.prepare_class_def(defn, info)
if self.analyze_typeddict_classdef(defn):
return

if self.analyze_namedtuple_classdef(defn):
Expand Down Expand Up @@ -1423,9 +1413,36 @@ def analyze_class_body_common(self, defn: ClassDef) -> None:
self.apply_class_plugin_hooks(defn)
self.leave_class()

def analyze_typeddict_classdef(self, defn: ClassDef) -> bool:
if (
defn.info
and defn.info.typeddict_type
and not has_placeholder(defn.info.typeddict_type)
):
# This is a valid TypedDict, and it is fully analyzed.
return True
is_typeddict, info = self.typed_dict_analyzer.analyze_typeddict_classdef(defn)
if is_typeddict:
for decorator in defn.decorators:
decorator.accept(self)
if isinstance(decorator, RefExpr):
if decorator.fullname in FINAL_DECORATOR_NAMES:
self.fail("@final cannot be used with TypedDict", decorator)
if info is None:
self.mark_incomplete(defn.name, defn)
else:
self.prepare_class_def(defn, info)
return True
return False

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 Down Expand Up @@ -1785,7 +1802,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 @@ -1828,21 +1845,17 @@ 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.
# Use equality to check if it is the case.
if info.tuple_type and info.tuple_type != base:
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 info.special_alias and has_placeholder(info.special_alias.target):
self.defer(force_progress=True)
info.update_tuple_type(base)

if base.partial_fallback.type.fullname == "builtins.tuple":
# Fallback can only be safely calculated after semantic analysis, since base
Expand Down Expand Up @@ -2627,7 +2640,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 @@ -2657,7 +2673,12 @@ def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
def analyze_typeddict_assign(self, s: AssignmentStmt) -> bool:
"""Check if s defines a typed dict."""
if isinstance(s.rvalue, CallExpr) and isinstance(s.rvalue.analyzed, TypedDictExpr):
return True # This is a valid and analyzed typed dict definition, nothing to do here.
if s.rvalue.analyzed.info.typeddict_type and not has_placeholder(
s.rvalue.analyzed.info.typeddict_type
):
return (
True # This is a valid and analyzed typed dict 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 @@ -3028,6 +3049,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 +3181,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 @@ -5484,7 +5507,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 @@ -5498,6 +5521,13 @@ 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:
# Usually, we report progress if we have replaced a placeholder node
# with an actual valid node. However, sometimes we need to update an
# existing node *in-place*. For example, this is used by type aliases
# in context of forward references and/or recursive aliases, and in
# similar situations (recursive named tuples etc).
self.progress = True
self.deferred = True
# Store debug info for this deferral.
line = (
Expand Down Expand Up @@ -5999,19 +6029,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
Loading