Skip to content

Commit 7ffa87f

Browse files
committed
Merge remote-tracking branch 'upstream/master' into issue-16614
2 parents 310c75d + 790e8a7 commit 7ffa87f

File tree

15 files changed

+404
-65
lines changed

15 files changed

+404
-65
lines changed

mypy/checker.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5053,6 +5053,19 @@ def visit_continue_stmt(self, s: ContinueStmt) -> None:
50535053
return
50545054

50555055
def visit_match_stmt(self, s: MatchStmt) -> None:
5056+
named_subject: Expression
5057+
if isinstance(s.subject, CallExpr):
5058+
# Create a dummy subject expression to handle cases where a match statement's subject
5059+
# is not a literal value. This lets us correctly narrow types and check exhaustivity
5060+
# This is hack!
5061+
id = s.subject.callee.fullname if isinstance(s.subject.callee, RefExpr) else ""
5062+
name = "dummy-match-" + id
5063+
v = Var(name)
5064+
named_subject = NameExpr(name)
5065+
named_subject.node = v
5066+
else:
5067+
named_subject = s.subject
5068+
50565069
with self.binder.frame_context(can_skip=False, fall_through=0):
50575070
subject_type = get_proper_type(self.expr_checker.accept(s.subject))
50585071

@@ -5071,7 +5084,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
50715084
# The second pass narrows down the types and type checks bodies.
50725085
for p, g, b in zip(s.patterns, s.guards, s.bodies):
50735086
current_subject_type = self.expr_checker.narrow_type_from_binder(
5074-
s.subject, subject_type
5087+
named_subject, subject_type
50755088
)
50765089
pattern_type = self.pattern_checker.accept(p, current_subject_type)
50775090
with self.binder.frame_context(can_skip=True, fall_through=2):
@@ -5082,7 +5095,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
50825095
else_map: TypeMap = {}
50835096
else:
50845097
pattern_map, else_map = conditional_types_to_typemaps(
5085-
s.subject, pattern_type.type, pattern_type.rest_type
5098+
named_subject, pattern_type.type, pattern_type.rest_type
50865099
)
50875100
self.remove_capture_conflicts(pattern_type.captures, inferred_types)
50885101
self.push_type_map(pattern_map)
@@ -5110,7 +5123,7 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
51105123
and expr.fullname == case_target.fullname
51115124
):
51125125
continue
5113-
type_map[s.subject] = type_map[expr]
5126+
type_map[named_subject] = type_map[expr]
51145127

51155128
self.push_type_map(guard_map)
51165129
self.accept(b)

mypy/messages.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2059,6 +2059,15 @@ def impossible_intersection(
20592059
template.format(formatted_base_class_list, reason), context, code=codes.UNREACHABLE
20602060
)
20612061

2062+
def tvar_without_default_type(
2063+
self, tvar_name: str, last_tvar_name_with_default: str, context: Context
2064+
) -> None:
2065+
self.fail(
2066+
f'"{tvar_name}" cannot appear after "{last_tvar_name_with_default}" '
2067+
"in type parameter list because it has no default type",
2068+
context,
2069+
)
2070+
20622071
def report_protocol_problems(
20632072
self,
20642073
subtype: Instance | TupleType | TypedDictType | TypeType | CallableType,

mypy/semanal.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@
226226
SELF_TYPE_NAMES,
227227
FindTypeVarVisitor,
228228
TypeAnalyser,
229+
TypeVarDefaultTranslator,
229230
TypeVarLikeList,
230231
analyze_type_alias,
231232
check_for_explicit_any,
@@ -252,6 +253,7 @@
252253
TPDICT_NAMES,
253254
TYPE_ALIAS_NAMES,
254255
TYPE_CHECK_ONLY_NAMES,
256+
TYPE_VAR_LIKE_NAMES,
255257
TYPED_NAMEDTUPLE_NAMES,
256258
AnyType,
257259
CallableType,
@@ -1953,17 +1955,19 @@ class Foo(Bar, Generic[T]): ...
19531955
defn.removed_base_type_exprs.append(defn.base_type_exprs[i])
19541956
del base_type_exprs[i]
19551957
tvar_defs: list[TypeVarLikeType] = []
1958+
last_tvar_name_with_default: str | None = None
19561959
for name, tvar_expr in declared_tvars:
1957-
tvar_expr_default = tvar_expr.default
1958-
if isinstance(tvar_expr_default, UnboundType):
1959-
# TODO: - detect out of order and self-referencing TypeVars
1960-
# - nested default types, e.g. list[T1]
1961-
n = self.lookup_qualified(
1962-
tvar_expr_default.name, tvar_expr_default, suppress_errors=True
1963-
)
1964-
if n is not None and (default := self.tvar_scope.get_binding(n)) is not None:
1965-
tvar_expr.default = default
1960+
tvar_expr.default = tvar_expr.default.accept(
1961+
TypeVarDefaultTranslator(self, tvar_expr.name, context)
1962+
)
19661963
tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
1964+
if last_tvar_name_with_default is not None and not tvar_def.has_default():
1965+
self.msg.tvar_without_default_type(
1966+
tvar_def.name, last_tvar_name_with_default, context
1967+
)
1968+
tvar_def.default = AnyType(TypeOfAny.from_error)
1969+
elif tvar_def.has_default():
1970+
last_tvar_name_with_default = tvar_def.name
19671971
tvar_defs.append(tvar_def)
19681972
return base_type_exprs, tvar_defs, is_protocol
19691973

@@ -2857,6 +2861,10 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
28572861
with self.allow_unbound_tvars_set():
28582862
s.rvalue.accept(self)
28592863
self.basic_type_applications = old_basic_type_applications
2864+
elif self.can_possibly_be_typevarlike_declaration(s):
2865+
# Allow unbound tvars inside TypeVarLike defaults to be evaluated later
2866+
with self.allow_unbound_tvars_set():
2867+
s.rvalue.accept(self)
28602868
else:
28612869
s.rvalue.accept(self)
28622870

@@ -3033,6 +3041,16 @@ def can_possibly_be_type_form(self, s: AssignmentStmt) -> bool:
30333041
# Something that looks like Foo = Bar[Baz, ...]
30343042
return True
30353043

3044+
def can_possibly_be_typevarlike_declaration(self, s: AssignmentStmt) -> bool:
3045+
"""Check if r.h.s. can be a TypeVarLike declaration."""
3046+
if len(s.lvalues) != 1 or not isinstance(s.lvalues[0], NameExpr):
3047+
return False
3048+
if not isinstance(s.rvalue, CallExpr) or not isinstance(s.rvalue.callee, NameExpr):
3049+
return False
3050+
ref = s.rvalue.callee
3051+
ref.accept(self)
3052+
return ref.fullname in TYPE_VAR_LIKE_NAMES
3053+
30363054
def is_type_ref(self, rv: Expression, bare: bool = False) -> bool:
30373055
"""Does this expression refer to a type?
30383056
@@ -3522,9 +3540,20 @@ def analyze_alias(
35223540
tvar_defs: list[TypeVarLikeType] = []
35233541
namespace = self.qualified_name(name)
35243542
alias_type_vars = found_type_vars if declared_type_vars is None else declared_type_vars
3543+
last_tvar_name_with_default: str | None = None
35253544
with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)):
35263545
for name, tvar_expr in alias_type_vars:
3546+
tvar_expr.default = tvar_expr.default.accept(
3547+
TypeVarDefaultTranslator(self, tvar_expr.name, typ)
3548+
)
35273549
tvar_def = self.tvar_scope.bind_new(name, tvar_expr)
3550+
if last_tvar_name_with_default is not None and not tvar_def.has_default():
3551+
self.msg.tvar_without_default_type(
3552+
tvar_def.name, last_tvar_name_with_default, typ
3553+
)
3554+
tvar_def.default = AnyType(TypeOfAny.from_error)
3555+
elif tvar_def.has_default():
3556+
last_tvar_name_with_default = tvar_def.name
35283557
tvar_defs.append(tvar_def)
35293558

35303559
analyzed, depends_on = analyze_type_alias(

mypy/stubgen.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
import os.path
4848
import sys
4949
import traceback
50-
from typing import Final, Iterable
50+
from typing import Final, Iterable, Iterator
5151

5252
import mypy.build
5353
import mypy.mixedtraverser
@@ -114,6 +114,7 @@
114114
from mypy.stubdoc import ArgSig, FunctionSig
115115
from mypy.stubgenc import InspectionStubGenerator, generate_stub_for_c_module
116116
from mypy.stubutil import (
117+
TYPING_BUILTIN_REPLACEMENTS,
117118
BaseStubGenerator,
118119
CantImport,
119120
ClassInfo,
@@ -289,20 +290,19 @@ def visit_call_expr(self, node: CallExpr) -> str:
289290
raise ValueError(f"Unknown argument kind {kind} in call")
290291
return f"{callee}({', '.join(args)})"
291292

293+
def _visit_ref_expr(self, node: NameExpr | MemberExpr) -> str:
294+
fullname = self.stubgen.get_fullname(node)
295+
if fullname in TYPING_BUILTIN_REPLACEMENTS:
296+
return self.stubgen.add_name(TYPING_BUILTIN_REPLACEMENTS[fullname], require=False)
297+
qualname = get_qualified_name(node)
298+
self.stubgen.import_tracker.require_name(qualname)
299+
return qualname
300+
292301
def visit_name_expr(self, node: NameExpr) -> str:
293-
self.stubgen.import_tracker.require_name(node.name)
294-
return node.name
302+
return self._visit_ref_expr(node)
295303

296304
def visit_member_expr(self, o: MemberExpr) -> str:
297-
node: Expression = o
298-
trailer = ""
299-
while isinstance(node, MemberExpr):
300-
trailer = "." + node.name + trailer
301-
node = node.expr
302-
if not isinstance(node, NameExpr):
303-
return ERROR_MARKER
304-
self.stubgen.import_tracker.require_name(node.name)
305-
return node.name + trailer
305+
return self._visit_ref_expr(o)
306306

307307
def visit_str_expr(self, node: StrExpr) -> str:
308308
return repr(node.value)
@@ -351,11 +351,17 @@ def find_defined_names(file: MypyFile) -> set[str]:
351351
return finder.names
352352

353353

354+
def get_assigned_names(lvalues: Iterable[Expression]) -> Iterator[str]:
355+
for lvalue in lvalues:
356+
if isinstance(lvalue, NameExpr):
357+
yield lvalue.name
358+
elif isinstance(lvalue, TupleExpr):
359+
yield from get_assigned_names(lvalue.items)
360+
361+
354362
class DefinitionFinder(mypy.traverser.TraverserVisitor):
355363
"""Find names of things defined at the top level of a module."""
356364

357-
# TODO: Assignment statements etc.
358-
359365
def __init__(self) -> None:
360366
# Short names of things defined at the top level.
361367
self.names: set[str] = set()
@@ -368,6 +374,10 @@ def visit_func_def(self, o: FuncDef) -> None:
368374
# Don't recurse, as we only keep track of top-level definitions.
369375
self.names.add(o.name)
370376

377+
def visit_assignment_stmt(self, o: AssignmentStmt) -> None:
378+
for name in get_assigned_names(o.lvalues):
379+
self.names.add(name)
380+
371381

372382
def find_referenced_names(file: MypyFile) -> set[str]:
373383
finder = ReferenceFinder()
@@ -1023,10 +1033,15 @@ def is_alias_expression(self, expr: Expression, top_level: bool = True) -> bool:
10231033
and isinstance(expr.node, (FuncDef, Decorator, MypyFile))
10241034
or isinstance(expr.node, TypeInfo)
10251035
) and not self.is_private_member(expr.node.fullname)
1026-
elif (
1027-
isinstance(expr, IndexExpr)
1028-
and isinstance(expr.base, NameExpr)
1029-
and not self.is_private_name(expr.base.name)
1036+
elif isinstance(expr, IndexExpr) and (
1037+
(isinstance(expr.base, NameExpr) and not self.is_private_name(expr.base.name))
1038+
or ( # Also some known aliases that could be member expression
1039+
isinstance(expr.base, MemberExpr)
1040+
and not self.is_private_member(get_qualified_name(expr.base))
1041+
and self.get_fullname(expr.base).startswith(
1042+
("builtins.", "typing.", "typing_extensions.", "collections.abc.")
1043+
)
1044+
)
10301045
):
10311046
if isinstance(expr.index, TupleExpr):
10321047
indices = expr.index.items

mypy/stubutil.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@
2222
# Modules that may fail when imported, or that may have side effects (fully qualified).
2323
NOT_IMPORTABLE_MODULES = ()
2424

25+
# Typing constructs to be replaced by their builtin equivalents.
26+
TYPING_BUILTIN_REPLACEMENTS: Final = {
27+
# From typing
28+
"typing.Text": "builtins.str",
29+
"typing.Tuple": "builtins.tuple",
30+
"typing.List": "builtins.list",
31+
"typing.Dict": "builtins.dict",
32+
"typing.Set": "builtins.set",
33+
"typing.FrozenSet": "builtins.frozenset",
34+
"typing.Type": "builtins.type",
35+
# From typing_extensions
36+
"typing_extensions.Text": "builtins.str",
37+
"typing_extensions.Tuple": "builtins.tuple",
38+
"typing_extensions.List": "builtins.list",
39+
"typing_extensions.Dict": "builtins.dict",
40+
"typing_extensions.Set": "builtins.set",
41+
"typing_extensions.FrozenSet": "builtins.frozenset",
42+
"typing_extensions.Type": "builtins.type",
43+
}
44+
2545

2646
class CantImport(Exception):
2747
def __init__(self, module: str, message: str) -> None:
@@ -229,6 +249,8 @@ def visit_unbound_type(self, t: UnboundType) -> str:
229249
return " | ".join([item.accept(self) for item in t.args])
230250
if fullname == "typing.Optional":
231251
return f"{t.args[0].accept(self)} | None"
252+
if fullname in TYPING_BUILTIN_REPLACEMENTS:
253+
s = self.stubgen.add_name(TYPING_BUILTIN_REPLACEMENTS[fullname], require=True)
232254
if self.known_modules is not None and "." in s:
233255
# see if this object is from any of the modules that we're currently processing.
234256
# reverse sort so that subpackages come before parents: e.g. "foo.bar" before "foo".
@@ -476,7 +498,7 @@ def reexport(self, name: str) -> None:
476498
def import_lines(self) -> list[str]:
477499
"""The list of required import lines (as strings with python code).
478500
479-
In order for a module be included in this output, an indentifier must be both
501+
In order for a module be included in this output, an identifier must be both
480502
'required' via require_name() and 'imported' via add_import_from()
481503
or add_import()
482504
"""
@@ -585,9 +607,9 @@ def __init__(
585607
# a corresponding import statement.
586608
self.known_imports = {
587609
"_typeshed": ["Incomplete"],
588-
"typing": ["Any", "TypeVar", "NamedTuple"],
610+
"typing": ["Any", "TypeVar", "NamedTuple", "TypedDict"],
589611
"collections.abc": ["Generator"],
590-
"typing_extensions": ["TypedDict", "ParamSpec", "TypeVarTuple"],
612+
"typing_extensions": ["ParamSpec", "TypeVarTuple"],
591613
}
592614

593615
def get_sig_generators(self) -> list[SignatureGenerator]:
@@ -613,7 +635,10 @@ def add_name(self, fullname: str, require: bool = True) -> str:
613635
"""
614636
module, name = fullname.rsplit(".", 1)
615637
alias = "_" + name if name in self.defined_names else None
616-
self.import_tracker.add_import_from(module, [(name, alias)], require=require)
638+
while alias in self.defined_names:
639+
alias = "_" + alias
640+
if module != "builtins" or alias: # don't import from builtins unless needed
641+
self.import_tracker.add_import_from(module, [(name, alias)], require=require)
617642
return alias or name
618643

619644
def add_import_line(self, line: str) -> None:

mypy/typeanal.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@
3838
)
3939
from mypy.options import Options
4040
from mypy.plugin import AnalyzeTypeContext, Plugin, TypeAnalyzerPluginInterface
41-
from mypy.semanal_shared import SemanticAnalyzerCoreInterface, paramspec_args, paramspec_kwargs
41+
from mypy.semanal_shared import (
42+
SemanticAnalyzerCoreInterface,
43+
SemanticAnalyzerInterface,
44+
paramspec_args,
45+
paramspec_kwargs,
46+
)
4247
from mypy.state import state
4348
from mypy.tvar_scope import TypeVarLikeScope
4449
from mypy.types import (
@@ -2520,3 +2525,32 @@ def process_types(self, types: list[Type] | tuple[Type, ...]) -> None:
25202525
else:
25212526
for t in types:
25222527
t.accept(self)
2528+
2529+
2530+
class TypeVarDefaultTranslator(TrivialSyntheticTypeTranslator):
2531+
"""Type translate visitor that replaces UnboundTypes with in-scope TypeVars."""
2532+
2533+
def __init__(
2534+
self, api: SemanticAnalyzerInterface, tvar_expr_name: str, context: Context
2535+
) -> None:
2536+
self.api = api
2537+
self.tvar_expr_name = tvar_expr_name
2538+
self.context = context
2539+
2540+
def visit_unbound_type(self, t: UnboundType) -> Type:
2541+
sym = self.api.lookup_qualified(t.name, t, suppress_errors=True)
2542+
if sym is not None:
2543+
if type_var := self.api.tvar_scope.get_binding(sym):
2544+
return type_var
2545+
if isinstance(sym.node, TypeVarLikeExpr):
2546+
self.api.fail(
2547+
f'Type parameter "{self.tvar_expr_name}" has a default type '
2548+
"that refers to one or more type variables that are out of scope",
2549+
self.context,
2550+
)
2551+
return AnyType(TypeOfAny.from_error)
2552+
return super().visit_unbound_type(t)
2553+
2554+
def visit_type_alias_type(self, t: TypeAliasType) -> Type:
2555+
# TypeAliasTypes are analyzed separately already, just return it
2556+
return t

mypy/types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@
8585
TypeVisitor as TypeVisitor,
8686
)
8787

88+
TYPE_VAR_LIKE_NAMES: Final = (
89+
"typing.TypeVar",
90+
"typing_extensions.TypeVar",
91+
"typing.ParamSpec",
92+
"typing_extensions.ParamSpec",
93+
"typing.TypeVarTuple",
94+
"typing_extensions.TypeVarTuple",
95+
)
96+
8897
TYPED_NAMEDTUPLE_NAMES: Final = ("typing.NamedTuple", "typing_extensions.NamedTuple")
8998

9099
# Supported names of TypedDict type constructors.

0 commit comments

Comments
 (0)