Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PEP 695] Implement new scoping rules for type parameters #17258

Merged
merged 12 commits into from
May 17, 2024
10 changes: 7 additions & 3 deletions mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2502,7 +2502,7 @@ class TypeVarLikeExpr(SymbolNode, Expression):
Note that they are constructed by the semantic analyzer.
"""

__slots__ = ("_name", "_fullname", "upper_bound", "default", "variance")
__slots__ = ("_name", "_fullname", "upper_bound", "default", "variance", "is_new_style")

_name: str
_fullname: str
Expand All @@ -2525,13 +2525,15 @@ def __init__(
upper_bound: mypy.types.Type,
default: mypy.types.Type,
variance: int = INVARIANT,
is_new_style: bool = False,
) -> None:
super().__init__()
self._name = name
self._fullname = fullname
self.upper_bound = upper_bound
self.default = default
self.variance = variance
self.is_new_style = is_new_style

@property
def name(self) -> str:
Expand Down Expand Up @@ -2570,8 +2572,9 @@ def __init__(
upper_bound: mypy.types.Type,
default: mypy.types.Type,
variance: int = INVARIANT,
is_new_style: bool = False,
) -> None:
super().__init__(name, fullname, upper_bound, default, variance)
super().__init__(name, fullname, upper_bound, default, variance, is_new_style)
self.values = values

def accept(self, visitor: ExpressionVisitor[T]) -> T:
Expand Down Expand Up @@ -2648,8 +2651,9 @@ def __init__(
tuple_fallback: mypy.types.Instance,
default: mypy.types.Type,
variance: int = INVARIANT,
is_new_style: bool = False,
) -> None:
super().__init__(name, fullname, upper_bound, default, variance)
super().__init__(name, fullname, upper_bound, default, variance, is_new_style)
self.tuple_fallback = tuple_fallback

def accept(self, visitor: ExpressionVisitor[T]) -> T:
Expand Down
95 changes: 67 additions & 28 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,14 @@
CORE_BUILTIN_CLASSES: Final = ["object", "bool", "function"]


# Python has several different scope/namespace kinds with subtly different semantics.
SCOPE_GLOBAL: Final = 0 # Module top level
SCOPE_CLASS: Final = 1 # Class body
SCOPE_FUNC: Final = 2 # Function or lambda
SCOPE_COMPREHENSION: Final = 3 # Comprehension or generator expression
SCOPE_TYPE_PARAM: Final = 4 # Python 3.12 new-style type parameter scope (PEP 695)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same scoping is used for type alias values and in the future will be used for annotations (under PEP 649). In CPython we're calling this "annotation scopes", I'd suggest using the same here:

Suggested change
SCOPE_TYPE_PARAM: Final = 4 # Python 3.12 new-style type parameter scope (PEP 695)
SCOPE_ANNOTATION: Final = 4 # Annotation scopes for type parameters and aliases (PEP 695)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed as suggested.



# Used for tracking incomplete references
Tag: _TypeAlias = int

Expand All @@ -342,8 +350,8 @@ class SemanticAnalyzer(
nonlocal_decls: list[set[str]]
# Local names of function scopes; None for non-function scopes.
locals: list[SymbolTable | None]
# Whether each scope is a comprehension scope.
is_comprehension_stack: list[bool]
# Type of each scope (SCOPE_*, indexes match locals)
scope_stack: list[int]
# Nested block depths of scopes
block_depth: list[int]
# TypeInfo of directly enclosing class (or None)
Expand Down Expand Up @@ -417,7 +425,7 @@ def __init__(
errors: Report analysis errors using this instance
"""
self.locals = [None]
self.is_comprehension_stack = [False]
self.scope_stack = [SCOPE_GLOBAL]
# Saved namespaces from previous iteration. Every top-level function/method body is
# analyzed in several iterations until all names are resolved. We need to save
# the local namespaces for the top level function and all nested functions between
Expand Down Expand Up @@ -880,6 +888,7 @@ def analyze_func_def(self, defn: FuncDef) -> None:
# Don't store not ready types (including placeholders).
if self.found_incomplete_ref(tag) or has_placeholder(result):
self.defer(defn)
# TODO: pop type args
return
assert isinstance(result, ProperType)
if isinstance(result, CallableType):
Expand Down Expand Up @@ -1645,6 +1654,8 @@ def push_type_args(
) -> list[tuple[str, TypeVarLikeExpr]] | None:
if not type_args:
return []
self.locals.append(SymbolTable())
self.scope_stack.append(SCOPE_TYPE_PARAM)
tvs: list[tuple[str, TypeVarLikeExpr]] = []
for p in type_args:
tv = self.analyze_type_param(p)
Expand All @@ -1653,7 +1664,7 @@ def push_type_args(
tvs.append((p.name, tv))

for name, tv in tvs:
self.add_symbol(name, tv, context, no_progress=True)
self.add_symbol(name, tv, context, no_progress=True, type_param=True)

return tvs

Expand Down Expand Up @@ -1681,10 +1692,15 @@ def analyze_type_param(self, type_param: TypeParam) -> TypeVarLikeExpr | None:
upper_bound=upper_bound,
default=default,
variance=VARIANCE_NOT_READY,
is_new_style=True,
)
elif type_param.kind == PARAM_SPEC_KIND:
return ParamSpecExpr(
name=type_param.name, fullname=fullname, upper_bound=upper_bound, default=default
name=type_param.name,
fullname=fullname,
upper_bound=upper_bound,
default=default,
is_new_style=True,
)
else:
assert type_param.kind == TYPE_VAR_TUPLE_KIND
Expand All @@ -1696,14 +1712,14 @@ def analyze_type_param(self, type_param: TypeParam) -> TypeVarLikeExpr | None:
upper_bound=tuple_fallback.copy_modified(),
tuple_fallback=tuple_fallback,
default=default,
is_new_style=True,
)

def pop_type_args(self, type_args: list[TypeParam] | None) -> None:
if not type_args:
return
for tv in type_args:
names = self.current_symbol_table()
del names[tv.name]
self.locals.pop()
self.scope_stack.pop()

def analyze_class(self, defn: ClassDef) -> None:
fullname = self.qualified_name(defn.name)
Expand Down Expand Up @@ -1938,7 +1954,7 @@ def enter_class(self, info: TypeInfo) -> None:
# Remember previous active class
self.type_stack.append(self.type)
self.locals.append(None) # Add class scope
self.is_comprehension_stack.append(False)
self.scope_stack.append(SCOPE_CLASS)
self.block_depth.append(-1) # The class body increments this to 0
self.loop_depth.append(0)
self._type = info
Expand All @@ -1949,7 +1965,7 @@ def leave_class(self) -> None:
self.block_depth.pop()
self.loop_depth.pop()
self.locals.pop()
self.is_comprehension_stack.pop()
self.scope_stack.pop()
self._type = self.type_stack.pop()
self.missing_names.pop()

Expand Down Expand Up @@ -2923,8 +2939,8 @@ class C:
[(j := i) for i in [1, 2, 3]]
is a syntax error that is not enforced by Python parser, but at later steps.
"""
for i, is_comprehension in enumerate(reversed(self.is_comprehension_stack)):
if not is_comprehension and i < len(self.locals) - 1:
for i, scope_type in enumerate(reversed(self.scope_stack)):
if scope_type != SCOPE_COMPREHENSION and i < len(self.locals) - 1:
if self.locals[-1 - i] is None:
self.fail(
"Assignment expression within a comprehension"
Expand Down Expand Up @@ -5188,8 +5204,14 @@ def visit_nonlocal_decl(self, d: NonlocalDecl) -> None:
self.fail("nonlocal declaration not allowed at module level", d)
else:
for name in d.names:
for table in reversed(self.locals[:-1]):
for table, scope_type in zip(
reversed(self.locals[:-1]), reversed(self.scope_stack[:-1])
):
if table is not None and name in table:
if scope_type == SCOPE_TYPE_PARAM:
self.fail(
f'nonlocal binding not allowed for type parameter "{name}"', d
)
break
else:
self.fail(f'No binding for nonlocal "{name}" found', d)
Expand Down Expand Up @@ -5350,7 +5372,7 @@ def visit_star_expr(self, expr: StarExpr) -> None:
def visit_yield_from_expr(self, e: YieldFromExpr) -> None:
if not self.is_func_scope():
self.fail('"yield from" outside function', e, serious=True, blocker=True)
elif self.is_comprehension_stack[-1]:
elif self.scope_stack[-1] == SCOPE_COMPREHENSION:
self.fail(
'"yield from" inside comprehension or generator expression',
e,
Expand Down Expand Up @@ -5848,7 +5870,7 @@ def visit__promote_expr(self, expr: PromoteExpr) -> None:
def visit_yield_expr(self, e: YieldExpr) -> None:
if not self.is_func_scope():
self.fail('"yield" outside function', e, serious=True, blocker=True)
elif self.is_comprehension_stack[-1]:
elif self.scope_stack[-1] == SCOPE_COMPREHENSION:
self.fail(
'"yield" inside comprehension or generator expression',
e,
Expand Down Expand Up @@ -6281,6 +6303,7 @@ def add_symbol(
can_defer: bool = True,
escape_comprehensions: bool = False,
no_progress: bool = False,
type_param: bool = False,
) -> bool:
"""Add symbol to the currently active symbol table.

Expand All @@ -6303,7 +6326,7 @@ def add_symbol(
kind, node, module_public=module_public, module_hidden=module_hidden
)
return self.add_symbol_table_node(
name, symbol, context, can_defer, escape_comprehensions, no_progress
name, symbol, context, can_defer, escape_comprehensions, no_progress, type_param
)

def add_symbol_skip_local(self, name: str, node: SymbolNode) -> None:
Expand Down Expand Up @@ -6336,6 +6359,7 @@ def add_symbol_table_node(
can_defer: bool = True,
escape_comprehensions: bool = False,
no_progress: bool = False,
type_param: bool = False,
) -> bool:
"""Add symbol table node to the currently active symbol table.

Expand All @@ -6355,7 +6379,9 @@ def add_symbol_table_node(
can_defer: if True, defer current target if adding a placeholder
context: error context (see above about None value)
"""
names = self.current_symbol_table(escape_comprehensions=escape_comprehensions)
names = self.current_symbol_table(
escape_comprehensions=escape_comprehensions, type_param=type_param
)
existing = names.get(name)
if isinstance(symbol.node, PlaceholderNode) and can_defer:
if context is not None:
Expand Down Expand Up @@ -6673,7 +6699,7 @@ def enter(
names = self.saved_locals.setdefault(function, SymbolTable())
self.locals.append(names)
is_comprehension = isinstance(function, (GeneratorExpr, DictionaryComprehension))
self.is_comprehension_stack.append(is_comprehension)
self.scope_stack.append(SCOPE_FUNC if not is_comprehension else SCOPE_COMPREHENSION)
self.global_decls.append(set())
self.nonlocal_decls.append(set())
# -1 since entering block will increment this to 0.
Expand All @@ -6684,19 +6710,22 @@ def enter(
yield
finally:
self.locals.pop()
self.is_comprehension_stack.pop()
self.scope_stack.pop()
self.global_decls.pop()
self.nonlocal_decls.pop()
self.block_depth.pop()
self.loop_depth.pop()
self.missing_names.pop()

def is_func_scope(self) -> bool:
return self.locals[-1] is not None
scope_type = self.scope_stack[-1]
if scope_type == SCOPE_TYPE_PARAM:
scope_type = self.scope_stack[-2]
return scope_type in (SCOPE_FUNC, SCOPE_COMPREHENSION)

def is_nested_within_func_scope(self) -> bool:
"""Are we underneath a function scope, even if we are in a nested class also?"""
return any(l is not None for l in self.locals)
return any(s in (SCOPE_FUNC, SCOPE_COMPREHENSION) for s in self.scope_stack)

def is_class_scope(self) -> bool:
return self.type is not None and not self.is_func_scope()
Expand All @@ -6713,14 +6742,24 @@ def current_symbol_kind(self) -> int:
kind = GDEF
return kind

def current_symbol_table(self, escape_comprehensions: bool = False) -> SymbolTable:
if self.is_func_scope():
assert self.locals[-1] is not None
def current_symbol_table(
self, escape_comprehensions: bool = False, type_param: bool = False
) -> SymbolTable:
if type_param and self.scope_stack[-1] == SCOPE_TYPE_PARAM:
n = self.locals[-1]
assert n is not None
return n
elif self.is_func_scope():
if self.scope_stack[-1] == SCOPE_TYPE_PARAM:
n = self.locals[-2]
else:
n = self.locals[-1]
assert n is not None
if escape_comprehensions:
assert len(self.locals) == len(self.is_comprehension_stack)
assert len(self.locals) == len(self.scope_stack)
# Retrieve the symbol table from the enclosing non-comprehension scope.
for i, is_comprehension in enumerate(reversed(self.is_comprehension_stack)):
if not is_comprehension:
for i, scope_type in enumerate(reversed(self.scope_stack)):
if scope_type != SCOPE_COMPREHENSION:
if i == len(self.locals) - 1: # The last iteration.
# The caller of the comprehension is in the global space.
names = self.globals
Expand All @@ -6734,7 +6773,7 @@ def current_symbol_table(self, escape_comprehensions: bool = False) -> SymbolTab
else:
assert False, "Should have at least one non-comprehension scope"
else:
names = self.locals[-1]
names = n
assert names is not None
elif self.type is not None:
names = self.type.names
Expand Down
37 changes: 23 additions & 14 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,7 @@ def analyze_unbound_type_without_type_info(
t = t.copy_modified(args=self.anal_array(t.args))
# TODO: Move this message building logic to messages.py.
notes: list[str] = []
error_code = codes.VALID_TYPE
if isinstance(sym.node, Var):
notes.append(
"See https://mypy.readthedocs.io/en/"
Expand All @@ -912,25 +913,33 @@ def analyze_unbound_type_without_type_info(
message = 'Module "{}" is not valid as a type'
notes.append("Perhaps you meant to use a protocol matching the module structure?")
elif unbound_tvar:
message = 'Type variable "{}" is unbound'
short = name.split(".")[-1]
notes.append(
(
'(Hint: Use "Generic[{}]" or "Protocol[{}]" base class'
' to bind "{}" inside a class)'
).format(short, short, short)
)
notes.append(
'(Hint: Use "{}" in function signature to bind "{}"'
" inside a function)".format(short, short)
)
assert isinstance(sym.node, TypeVarLikeExpr)
if sym.node.is_new_style:
# PEP 695 type paramaters are never considered unbound -- they are undefined
# in contexts where they aren't valid, such as in argument default values.
message = 'Name "{}" is not defined'
name = name.split(".")[-1]
error_code = codes.NAME_DEFINED
else:
message = 'Type variable "{}" is unbound'
short = name.split(".")[-1]
notes.append(
(
'(Hint: Use "Generic[{}]" or "Protocol[{}]" base class'
' to bind "{}" inside a class)'
).format(short, short, short)
)
notes.append(
'(Hint: Use "{}" in function signature to bind "{}"'
" inside a function)".format(short, short)
)
else:
message = 'Cannot interpret reference "{}" as a type'
if not defining_literal:
# Literal check already gives a custom error. Avoid duplicating errors.
self.fail(message.format(name), t, code=codes.VALID_TYPE)
self.fail(message.format(name), t, code=error_code)
for note in notes:
self.note(note, t, code=codes.VALID_TYPE)
self.note(note, t, code=error_code)

# TODO: Would it be better to always return Any instead of UnboundType
# in case of an error? On one hand, UnboundType has a name so error messages
Expand Down
Loading
Loading