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
Prev Previous commit
Next Next commit
More cleanup and tests
  • Loading branch information
ilevkivskyi committed Aug 9, 2022
commit 3aeb47075e029772b227229f1659fc96542534f1
10 changes: 10 additions & 0 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2970,6 +2970,15 @@ 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 tuple_alias as needed."""
self.tuple_type = typ
alias = TypeAlias.from_tuple_type(self)
if not self.tuple_alias:
self.tuple_alias = alias
else:
self.tuple_alias.target = alias.target

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

Expand Down Expand Up @@ -3260,6 +3269,7 @@ def __init__(

@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, [])),
Expand Down
11 changes: 3 additions & 8 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -1840,14 +1840,9 @@ def configure_tuple_base_class(self, defn: ClassDef, base: TupleType) -> Instanc
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
alias = TypeAlias.from_tuple_type(info)
if not info.tuple_alias:
info.tuple_alias = alias
else:
if has_placeholder(info.tuple_alias.target):
self.defer(force_progress=True)
info.tuple_alias.target = alias.target
if info.tuple_alias and has_placeholder(info.tuple_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
22 changes: 8 additions & 14 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
SymbolTableNode,
TempNode,
TupleExpr,
TypeAlias,
TypeInfo,
TypeVarExpr,
Var,
Expand All @@ -52,7 +51,6 @@
TYPED_NAMEDTUPLE_NAMES,
AnyType,
CallableType,
Instance,
LiteralType,
TupleType,
Type,
Expand Down Expand Up @@ -170,6 +168,9 @@ def check_namedtuple_classdef(
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=self.options.enable_recursive_aliases
Expand Down Expand Up @@ -428,6 +429,7 @@ def parse_namedtuple_fields_with_types(
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=self.options.enable_recursive_aliases
Expand Down Expand Up @@ -476,22 +478,13 @@ def build_namedtuple_typeinfo(
info = existing_info or self.api.basic_new_typeinfo(name, fallback, line)
info.is_named_tuple = True
tuple_base = TupleType(types, fallback)
old_tuple_type = None
if existing_info:
old_tuple_type = existing_info.tuple_type
info.tuple_type = tuple_base
alias = TypeAlias.from_tuple_type(info)
if not info.tuple_alias:
info.tuple_alias = alias
else:
info.tuple_alias.target = alias.target
if info.tuple_alias and has_placeholder(info.tuple_alias.target):
self.api.defer(force_progress=True)
info.update_tuple_type(tuple_base)
info.line = line
# For use by mypyc.
info.metadata["namedtuple"] = {"fields": items.copy()}

if old_tuple_type and has_placeholder(old_tuple_type):
self.api.defer(force_progress=True)

# 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.
Expand Down Expand Up @@ -528,6 +521,7 @@ def add_field(
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(
SELF_TVAR_NAME, info.fullname + "." + SELF_TVAR_NAME, -1, [], info.tuple_type
)
Expand Down
8 changes: 1 addition & 7 deletions mypy/semanal_newtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
RefExpr,
StrExpr,
SymbolTableNode,
TypeAlias,
TypeInfo,
Var,
)
Expand Down Expand Up @@ -94,12 +93,7 @@ def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
newtype_class_info = self.build_newtype_typeinfo(
name, old_type, old_type.partial_fallback, s.line, call.analyzed.info
)
newtype_class_info.tuple_type = old_type
alias = TypeAlias.from_tuple_type(newtype_class_info)
if not newtype_class_info.tuple_alias:
newtype_class_info.tuple_alias = alias
else:
newtype_class_info.tuple_alias.target = alias.target
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)
Expand Down
8 changes: 4 additions & 4 deletions mypy/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ def is_future_flag_set(self, flag: str) -> bool:
def is_stub_file(self) -> bool:
raise NotImplementedError

@abstractmethod
def is_func_scope(self) -> bool:
raise NotImplementedError


@trait
class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface):
Expand Down Expand Up @@ -211,10 +215,6 @@ def qualified_name(self, n: str) -> str:
def is_typeshed_stub_file(self) -> bool:
raise NotImplementedError

@abstractmethod
def is_func_scope(self) -> bool:
raise NotImplementedError


def set_callable_name(sig: Type, fdef: FuncDef) -> ProperType:
sig = get_proper_type(sig)
Expand Down
3 changes: 3 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,8 @@ def cannot_resolve_type(self, t: UnboundType) -> None:
# need access to MessageBuilder here. Also move the similar
# message generation logic in semanal.py.
self.api.fail(f'Cannot resolve name "{t.name}" (possible cyclic definition)', t)
if self.options.enable_recursive_aliases and self.api.is_func_scope():
self.note("Recursive types are not allowed at function scope", t)

def apply_concatenate_operator(self, t: UnboundType) -> Type:
if len(t.args) == 0:
Expand Down Expand Up @@ -611,6 +613,7 @@ def analyze_type_with_type_info(
self.fail("Generic tuple types not supported", ctx)
return AnyType(TypeOfAny.from_error)
if info.tuple_alias:
# We don't support generic tuple types yet.
return TypeAliasType(info.tuple_alias, [])
return tup.copy_modified(items=self.anal_array(tup.items), fallback=instance)
td = info.typeddict_type
Expand Down
16 changes: 14 additions & 2 deletions test-data/unit/check-recursive-types.test
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ class B(NamedTuple):
A = NamedTuple('A', [('x', str), ('y', B)])
n: B
m: A
reveal_type(n.x) # N: Revealed type is "Tuple[Tuple[builtins.str, Tuple[Tuple[..., builtins.int], builtins.int, fallback=__main__.B], fallback=__main__.A], builtins.int]"
s: str = n.x # E: Incompatible types in assignment (expression has type "Tuple[A, int]", variable has type "str")
reveal_type(m[0]) # N: Revealed type is "builtins.str"
lst = [m, n]
reveal_type(lst[0]) # N: Revealed type is "Tuple[builtins.object, builtins.object]"
Expand All @@ -563,7 +563,7 @@ class A(NamedTuple):
y: B

n: A
reveal_type(n.y[0]) # N: Revealed type is "Tuple[builtins.str, Tuple[Tuple[builtins.str, ..., fallback=__main__.A], builtins.int, fallback=__main__.B], fallback=__main__.A]"
s: str = n.y[0] # E: Incompatible types in assignment (expression has type "A", variable has type "str")

m: B
n = m.x
Expand All @@ -587,4 +587,16 @@ reveal_type(f) # N: Revealed type is "def (m: Tuple[Tuple[builtins.str, ..., fal
f(n) # E: Argument 1 to "f" has incompatible type "A"; expected "B"
[builtins fixtures/tuple.pyi]

[case testNoRecursiveTuplesAtFunctionScope]
# flags: --enable-recursive-aliases
from typing import NamedTuple, Tuple
def foo() -> None:
class B(NamedTuple):
x: B # E: Cannot resolve name "B" (possible cyclic definition) \
# N: Recursive types are not allowed at function scope
y: int
b: B
reveal_type(b) # N: Revealed type is "Tuple[Any, builtins.int, fallback=__main__.B@4]"
[builtins fixtures/tuple.pyi]

-- TODO: Add more fine-grained tests for various kinds of recursive types