Skip to content

Commit dc96406

Browse files
committed
Support new union syntax in stubs always in runtime context
Previously it only worked when the target Python version was 3.10. Work on #9880.
1 parent 56618b9 commit dc96406

10 files changed

+60
-39
lines changed

mypy/checker.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -2023,13 +2023,9 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None:
20232023
20242024
Handle all kinds of assignment statements (simple, indexed, multiple).
20252025
"""
2026-
with self.enter_final_context(s.is_final_def):
2027-
self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)
2028-
2029-
if s.is_alias_def:
2030-
# We do this mostly for compatibility with old semantic analyzer.
2031-
# TODO: should we get rid of this?
2032-
self.store_type(s.lvalues[-1], self.expr_checker.accept(s.rvalue))
2026+
if not (s.is_alias_def and self.is_stub):
2027+
with self.enter_final_context(s.is_final_def):
2028+
self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)
20332029

20342030
if (s.type is not None and
20352031
self.options.disallow_any_unimported and

mypy/exprtotype.py

+16-11
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,15 @@ def _extract_argument_name(expr: Expression) -> Optional[str]:
3232

3333
def expr_to_unanalyzed_type(expr: Expression,
3434
options: Optional[Options] = None,
35+
allow_new_syntax: bool = False,
3536
_parent: Optional[Expression] = None) -> ProperType:
3637
"""Translate an expression to the corresponding type.
3738
3839
The result is not semantically analyzed. It can be UnboundType or TypeList.
3940
Raise TypeTranslationError if the expression cannot represent a type.
41+
42+
If allow_new_syntax is True, allow all type syntax independent of the target
43+
Python version (used in stubs).
4044
"""
4145
# The `parent` parameter is used in recursive calls to provide context for
4246
# understanding whether an CallableArgument is ok.
@@ -56,7 +60,7 @@ def expr_to_unanalyzed_type(expr: Expression,
5660
else:
5761
raise TypeTranslationError()
5862
elif isinstance(expr, IndexExpr):
59-
base = expr_to_unanalyzed_type(expr.base, options, expr)
63+
base = expr_to_unanalyzed_type(expr.base, options, allow_new_syntax, expr)
6064
if isinstance(base, UnboundType):
6165
if base.args:
6266
raise TypeTranslationError()
@@ -72,20 +76,20 @@ def expr_to_unanalyzed_type(expr: Expression,
7276
# of the Annotation definition and only returning the type information,
7377
# losing all the annotations.
7478

75-
return expr_to_unanalyzed_type(args[0], options, expr)
79+
return expr_to_unanalyzed_type(args[0], options, allow_new_syntax, expr)
7680
else:
77-
base.args = tuple(expr_to_unanalyzed_type(arg, options, expr) for arg in args)
81+
base.args = tuple(expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr)
82+
for arg in args)
7883
if not base.args:
7984
base.empty_tuple_index = True
8085
return base
8186
else:
8287
raise TypeTranslationError()
8388
elif (isinstance(expr, OpExpr)
8489
and expr.op == '|'
85-
and options
86-
and options.python_version >= (3, 10)):
87-
return UnionType([expr_to_unanalyzed_type(expr.left, options),
88-
expr_to_unanalyzed_type(expr.right, options)])
90+
and ((options and options.python_version >= (3, 10)) or allow_new_syntax)):
91+
return UnionType([expr_to_unanalyzed_type(expr.left, options, allow_new_syntax),
92+
expr_to_unanalyzed_type(expr.right, options, allow_new_syntax)])
8993
elif isinstance(expr, CallExpr) and isinstance(_parent, ListExpr):
9094
c = expr.callee
9195
names = []
@@ -118,19 +122,20 @@ def expr_to_unanalyzed_type(expr: Expression,
118122
if typ is not default_type:
119123
# Two types
120124
raise TypeTranslationError()
121-
typ = expr_to_unanalyzed_type(arg, options, expr)
125+
typ = expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr)
122126
continue
123127
else:
124128
raise TypeTranslationError()
125129
elif i == 0:
126-
typ = expr_to_unanalyzed_type(arg, options, expr)
130+
typ = expr_to_unanalyzed_type(arg, options, allow_new_syntax, expr)
127131
elif i == 1:
128132
name = _extract_argument_name(arg)
129133
else:
130134
raise TypeTranslationError()
131135
return CallableArgument(typ, name, arg_const, expr.line, expr.column)
132136
elif isinstance(expr, ListExpr):
133-
return TypeList([expr_to_unanalyzed_type(t, options, expr) for t in expr.items],
137+
return TypeList([expr_to_unanalyzed_type(t, options, allow_new_syntax, expr)
138+
for t in expr.items],
134139
line=expr.line, column=expr.column)
135140
elif isinstance(expr, StrExpr):
136141
return parse_type_string(expr.value, 'builtins.str', expr.line, expr.column,
@@ -142,7 +147,7 @@ def expr_to_unanalyzed_type(expr: Expression,
142147
return parse_type_string(expr.value, 'builtins.unicode', expr.line, expr.column,
143148
assume_str_is_unicode=True)
144149
elif isinstance(expr, UnaryExpr):
145-
typ = expr_to_unanalyzed_type(expr.expr, options)
150+
typ = expr_to_unanalyzed_type(expr.expr, options, allow_new_syntax)
146151
if isinstance(typ, RawExpressionType):
147152
if isinstance(typ.literal_value, int) and expr.op == '-':
148153
typ.literal_value *= -1

mypy/plugin.py

+5
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,11 @@ def final_iteration(self) -> bool:
357357
"""Is this the final iteration of semantic analysis?"""
358358
raise NotImplementedError
359359

360+
@property
361+
@abstractmethod
362+
def is_stub_file(self) -> bool:
363+
raise NotImplementedError
364+
360365

361366
# A context for querying for configuration data about a module for
362367
# cache invalidation purposes.

mypy/plugins/attrs.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,7 @@ def _attribute_from_attrib_maker(ctx: 'mypy.plugin.ClassDefContext',
552552
type_arg = _get_argument(rvalue, 'type')
553553
if type_arg and not init_type:
554554
try:
555-
un_type = expr_to_unanalyzed_type(type_arg, ctx.api.options)
555+
un_type = expr_to_unanalyzed_type(type_arg, ctx.api.options, ctx.api.is_stub_file)
556556
except TypeTranslationError:
557557
ctx.api.fail('Invalid argument to type', type_arg)
558558
else:

mypy/semanal.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,7 @@ class Foo(Bar, Generic[T]): ...
12671267
self.analyze_type_expr(base_expr)
12681268

12691269
try:
1270-
base = expr_to_unanalyzed_type(base_expr, self.options)
1270+
base = self.expr_to_unanalyzed_type(base_expr)
12711271
except TypeTranslationError:
12721272
# This error will be caught later.
12731273
continue
@@ -1373,7 +1373,7 @@ def get_all_bases_tvars(self,
13731373
for i, base_expr in enumerate(base_type_exprs):
13741374
if i not in removed:
13751375
try:
1376-
base = expr_to_unanalyzed_type(base_expr, self.options)
1376+
base = self.expr_to_unanalyzed_type(base_expr)
13771377
except TypeTranslationError:
13781378
# This error will be caught later.
13791379
continue
@@ -2507,7 +2507,7 @@ def analyze_alias(self, rvalue: Expression,
25072507
self.plugin,
25082508
self.options,
25092509
self.is_typeshed_stub_file,
2510-
allow_unnormalized=self.is_stub_file,
2510+
allow_new_syntax=self.is_stub_file,
25112511
allow_placeholder=allow_placeholder,
25122512
in_dynamic_func=dynamic,
25132513
global_scope=global_scope)
@@ -3202,7 +3202,7 @@ def analyze_value_types(self, items: List[Expression]) -> List[Type]:
32023202
result: List[Type] = []
32033203
for node in items:
32043204
try:
3205-
analyzed = self.anal_type(expr_to_unanalyzed_type(node, self.options),
3205+
analyzed = self.anal_type(self.expr_to_unanalyzed_type(node),
32063206
allow_placeholder=True)
32073207
if analyzed is None:
32083208
# Type variables are special: we need to place them in the symbol table
@@ -3645,7 +3645,7 @@ def visit_call_expr(self, expr: CallExpr) -> None:
36453645
return
36463646
# Translate first argument to an unanalyzed type.
36473647
try:
3648-
target = expr_to_unanalyzed_type(expr.args[0], self.options)
3648+
target = self.expr_to_unanalyzed_type(expr.args[0])
36493649
except TypeTranslationError:
36503650
self.fail('Cast target is not a type', expr)
36513651
return
@@ -3703,7 +3703,7 @@ def visit_call_expr(self, expr: CallExpr) -> None:
37033703
return
37043704
# Translate first argument to an unanalyzed type.
37053705
try:
3706-
target = expr_to_unanalyzed_type(expr.args[0], self.options)
3706+
target = self.expr_to_unanalyzed_type(expr.args[0])
37073707
except TypeTranslationError:
37083708
self.fail('Argument 1 to _promote is not a type', expr)
37093709
return
@@ -3899,7 +3899,7 @@ def analyze_type_application_args(self, expr: IndexExpr) -> Optional[List[Type]]
38993899
items = [index]
39003900
for item in items:
39013901
try:
3902-
typearg = expr_to_unanalyzed_type(item, self.options)
3902+
typearg = self.expr_to_unanalyzed_type(item)
39033903
except TypeTranslationError:
39043904
self.fail('Type expected within [...]', expr)
39053905
return None
@@ -4206,7 +4206,7 @@ def lookup_qualified(self, name: str, ctx: Context,
42064206

42074207
def lookup_type_node(self, expr: Expression) -> Optional[SymbolTableNode]:
42084208
try:
4209-
t = expr_to_unanalyzed_type(expr, self.options)
4209+
t = self.expr_to_unanalyzed_type(expr)
42104210
except TypeTranslationError:
42114211
return None
42124212
if isinstance(t, UnboundType):
@@ -4926,7 +4926,7 @@ def expr_to_analyzed_type(self,
49264926
assert info.tuple_type, "NamedTuple without tuple type"
49274927
fallback = Instance(info, [])
49284928
return TupleType(info.tuple_type.items, fallback=fallback)
4929-
typ = expr_to_unanalyzed_type(expr, self.options)
4929+
typ = self.expr_to_unanalyzed_type(expr)
49304930
return self.anal_type(typ, report_invalid_types=report_invalid_types,
49314931
allow_placeholder=allow_placeholder)
49324932

@@ -4956,12 +4956,15 @@ def type_analyzer(self, *,
49564956
allow_unbound_tvars=allow_unbound_tvars,
49574957
allow_tuple_literal=allow_tuple_literal,
49584958
report_invalid_types=report_invalid_types,
4959-
allow_unnormalized=self.is_stub_file,
4959+
allow_new_syntax=self.is_stub_file,
49604960
allow_placeholder=allow_placeholder)
49614961
tpan.in_dynamic_func = bool(self.function_stack and self.function_stack[-1].is_dynamic())
49624962
tpan.global_scope = not self.type and not self.function_stack
49634963
return tpan
49644964

4965+
def expr_to_unanalyzed_type(self, node: Expression) -> ProperType:
4966+
return expr_to_unanalyzed_type(node, self.options, self.is_stub_file)
4967+
49654968
def anal_type(self,
49664969
typ: Type, *,
49674970
tvar_scope: Optional[TypeVarLikeScope] = None,

mypy/semanal_namedtuple.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: C
356356
self.fail("Invalid NamedTuple() field name", item)
357357
return None
358358
try:
359-
type = expr_to_unanalyzed_type(type_node, self.options)
359+
type = expr_to_unanalyzed_type(type_node, self.options, self.api.is_stub_file)
360360
except TypeTranslationError:
361361
self.fail('Invalid field type', type_node)
362362
return None

mypy/semanal_newtype.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def check_newtype_args(self, name: str, call: CallExpr,
160160
# Check second argument
161161
msg = "Argument 2 to NewType(...) must be a valid type"
162162
try:
163-
unanalyzed_type = expr_to_unanalyzed_type(args[1], self.options)
163+
unanalyzed_type = expr_to_unanalyzed_type(args[1], self.options, self.api.is_stub_file)
164164
except TypeTranslationError:
165165
self.fail(msg, context)
166166
return None, False

mypy/semanal_typeddict.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,8 @@ def parse_typeddict_fields_with_types(
290290
self.fail_typeddict_arg("Invalid TypedDict() field name", name_context)
291291
return [], [], False
292292
try:
293-
type = expr_to_unanalyzed_type(field_type_expr, self.options)
293+
type = expr_to_unanalyzed_type(field_type_expr, self.options,
294+
self.api.is_stub_file)
294295
except TypeTranslationError:
295296
self.fail_typeddict_arg('Invalid field type', field_type_expr)
296297
return [], [], False

mypy/typeanal.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def analyze_type_alias(node: Expression,
6969
plugin: Plugin,
7070
options: Options,
7171
is_typeshed_stub: bool,
72-
allow_unnormalized: bool = False,
72+
allow_new_syntax: bool = False,
7373
allow_placeholder: bool = False,
7474
in_dynamic_func: bool = False,
7575
global_scope: bool = True) -> Optional[Tuple[Type, Set[str]]]:
@@ -80,12 +80,12 @@ def analyze_type_alias(node: Expression,
8080
Return None otherwise. 'node' must have been semantically analyzed.
8181
"""
8282
try:
83-
type = expr_to_unanalyzed_type(node, options)
83+
type = expr_to_unanalyzed_type(node, options, allow_new_syntax)
8484
except TypeTranslationError:
8585
api.fail('Invalid type alias: expression is not a valid type', node)
8686
return None
8787
analyzer = TypeAnalyser(api, tvar_scope, plugin, options, is_typeshed_stub,
88-
allow_unnormalized=allow_unnormalized, defining_alias=True,
88+
allow_new_syntax=allow_new_syntax, defining_alias=True,
8989
allow_placeholder=allow_placeholder)
9090
analyzer.in_dynamic_func = in_dynamic_func
9191
analyzer.global_scope = global_scope
@@ -126,7 +126,7 @@ def __init__(self,
126126
is_typeshed_stub: bool, *,
127127
defining_alias: bool = False,
128128
allow_tuple_literal: bool = False,
129-
allow_unnormalized: bool = False,
129+
allow_new_syntax: bool = False,
130130
allow_unbound_tvars: bool = False,
131131
allow_placeholder: bool = False,
132132
report_invalid_types: bool = True) -> None:
@@ -143,7 +143,7 @@ def __init__(self,
143143
self.nesting_level = 0
144144
# Should we allow unnormalized types like `list[int]`
145145
# (currently allowed in stubs)?
146-
self.allow_unnormalized = allow_unnormalized
146+
self.allow_new_syntax = allow_new_syntax
147147
# Should we accept unbound type variables (always OK in aliases)?
148148
self.allow_unbound_tvars = allow_unbound_tvars or defining_alias
149149
# If false, record incomplete ref if we generate PlaceholderType.
@@ -199,7 +199,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool)
199199
return hook(AnalyzeTypeContext(t, t, self))
200200
if (fullname in get_nongen_builtins(self.options.python_version)
201201
and t.args and
202-
not self.allow_unnormalized and
202+
not self.allow_new_syntax and
203203
not self.api.is_future_flag_set("annotations")):
204204
self.fail(no_subscript_builtin_alias(fullname,
205205
propose_alt=not self.defining_alias), t)
@@ -282,7 +282,7 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
282282
elif (fullname == 'typing.Tuple' or
283283
(fullname == 'builtins.tuple' and (self.options.python_version >= (3, 9) or
284284
self.api.is_future_flag_set('annotations') or
285-
self.allow_unnormalized))):
285+
self.allow_new_syntax))):
286286
# Tuple is special because it is involved in builtin import cycle
287287
# and may be not ready when used.
288288
sym = self.api.lookup_fully_qualified_or_none('builtins.tuple')

test-data/unit/check-union-or-syntax.test

+11
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,14 @@ def f() -> object: pass
155155
reveal_type(cast(str | None, f())) # N: Revealed type is "Union[builtins.str, None]"
156156
reveal_type(list[str | None]()) # N: Revealed type is "builtins.list[Union[builtins.str, None]]"
157157
[builtins fixtures/type.pyi]
158+
159+
[case testUnionOrSyntaxRuntimeContextInStubFile]
160+
import lib
161+
reveal_type(lib.x) # N: Revealed type is "Union[builtins.int, builtins.list[builtins.str], None]"
162+
163+
[file lib.pyi]
164+
A = int | list[str] | None
165+
x: A
166+
class C(list[int | None]):
167+
pass
168+
[builtins fixtures/list.pyi]

0 commit comments

Comments
 (0)