forked from python/mypy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsemanal_newtype.py
273 lines (240 loc) · 10.3 KB
/
semanal_newtype.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
"""Semantic analysis of NewType definitions.
This is conceptually part of mypy.semanal (semantic analyzer pass 2).
"""
from __future__ import annotations
from mypy import errorcodes as codes
from mypy.errorcodes import ErrorCode
from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type
from mypy.messages import MessageBuilder, format_type
from mypy.nodes import (
ARG_POS,
MDEF,
Argument,
AssignmentStmt,
Block,
CallExpr,
Context,
FuncDef,
NameExpr,
NewTypeExpr,
PlaceholderNode,
RefExpr,
StrExpr,
SymbolTableNode,
TypeInfo,
Var,
)
from mypy.options import Options
from mypy.semanal_shared import SemanticAnalyzerInterface, has_placeholder
from mypy.typeanal import check_for_explicit_any, has_any_from_unimported_type
from mypy.types import (
AnyType,
CallableType,
Instance,
NoneType,
PlaceholderType,
TupleType,
Type,
TypeOfAny,
get_proper_type,
)
class NewTypeAnalyzer:
def __init__(
self, options: Options, api: SemanticAnalyzerInterface, msg: MessageBuilder
) -> None:
self.options = options
self.api = api
self.msg = msg
def process_newtype_declaration(self, s: AssignmentStmt) -> bool:
"""Check if s declares a NewType; if yes, store it in symbol table.
Return True if it's a NewType declaration. The current target may be
deferred as a side effect if the base type is not ready, even if
the return value is True.
The logic in this function mostly copies the logic for visit_class_def()
with a single (non-Generic) base.
"""
var_name, call = self.analyze_newtype_declaration(s)
if var_name is None or call is None:
return False
name = var_name
# OK, now we know this is a NewType. But the base type may be not ready yet,
# add placeholder as we do for ClassDef.
if self.api.is_func_scope():
name += "@" + str(s.line)
fullname = self.api.qualified_name(name)
if not call.analyzed or isinstance(call.analyzed, NewTypeExpr) and not call.analyzed.info:
# Start from labeling this as a future class, as we do for normal ClassDefs.
placeholder = PlaceholderNode(fullname, s, s.line, becomes_typeinfo=True)
self.api.add_symbol(var_name, placeholder, s, can_defer=False)
old_type, should_defer = self.check_newtype_args(var_name, call, s)
old_type = get_proper_type(old_type)
if not isinstance(call.analyzed, NewTypeExpr):
call.analyzed = NewTypeExpr(var_name, old_type, line=call.line, column=call.column)
else:
call.analyzed.old_type = old_type
if old_type is None:
if should_defer:
# Base type is not ready.
self.api.defer()
return True
# Create the corresponding class definition if the aliased type is subtypeable
assert isinstance(call.analyzed, NewTypeExpr)
if isinstance(old_type, TupleType):
newtype_class_info = self.build_newtype_typeinfo(
name, old_type, old_type.partial_fallback, s.line, call.analyzed.info
)
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)
newtype_class_info = self.build_newtype_typeinfo(
name, old_type, old_type, s.line, call.analyzed.info
)
else:
if old_type is not None:
message = "Argument 2 to NewType(...) must be subclassable (got {})"
self.fail(
message.format(format_type(old_type, self.options)),
s,
code=codes.VALID_NEWTYPE,
)
# Otherwise the error was already reported.
old_type = AnyType(TypeOfAny.from_error)
object_type = self.api.named_type("builtins.object")
newtype_class_info = self.build_newtype_typeinfo(
name, old_type, object_type, s.line, call.analyzed.info
)
newtype_class_info.fallback_to_any = True
check_for_explicit_any(
old_type, self.options, self.api.is_typeshed_stub_file, self.msg, context=s
)
if self.options.disallow_any_unimported and has_any_from_unimported_type(old_type):
self.msg.unimported_type_becomes_any("Argument 2 to NewType(...)", old_type, s)
# If so, add it to the symbol table.
assert isinstance(call.analyzed, NewTypeExpr)
# As we do for normal classes, create the TypeInfo only once, then just
# update base classes on next iterations (to get rid of placeholders there).
if not call.analyzed.info:
call.analyzed.info = newtype_class_info
else:
call.analyzed.info.bases = newtype_class_info.bases
self.api.add_symbol(var_name, call.analyzed.info, s)
if self.api.is_func_scope():
self.api.add_symbol_skip_local(name, call.analyzed.info)
newtype_class_info.line = s.line
return True
def analyze_newtype_declaration(self, s: AssignmentStmt) -> tuple[str | None, CallExpr | None]:
"""Return the NewType call expression if `s` is a newtype declaration or None otherwise."""
name, call = None, None
if (
len(s.lvalues) == 1
and isinstance(s.lvalues[0], NameExpr)
and isinstance(s.rvalue, CallExpr)
and isinstance(s.rvalue.callee, RefExpr)
and (s.rvalue.callee.fullname in ("typing.NewType", "typing_extensions.NewType"))
):
name = s.lvalues[0].name
if s.type:
self.fail("Cannot declare the type of a NewType declaration", s)
names = self.api.current_symbol_table()
existing = names.get(name)
# Give a better error message than generic "Name already defined".
if (
existing
and not isinstance(existing.node, PlaceholderNode)
and not s.rvalue.analyzed
):
self.fail(f'Cannot redefine "{name}" as a NewType', s)
# This dummy NewTypeExpr marks the call as sufficiently analyzed; it will be
# overwritten later with a fully complete NewTypeExpr if there are no other
# errors with the NewType() call.
call = s.rvalue
return name, call
def check_newtype_args(
self, name: str, call: CallExpr, context: Context
) -> tuple[Type | None, bool]:
"""Ananlyze base type in NewType call.
Return a tuple (type, should defer).
"""
has_failed = False
args, arg_kinds = call.args, call.arg_kinds
if len(args) != 2 or arg_kinds[0] != ARG_POS or arg_kinds[1] != ARG_POS:
self.fail("NewType(...) expects exactly two positional arguments", context)
return None, False
# Check first argument
if not isinstance(args[0], StrExpr):
self.fail("Argument 1 to NewType(...) must be a string literal", context)
has_failed = True
elif args[0].value != name:
msg = 'String argument 1 "{}" to NewType(...) does not match variable name "{}"'
self.fail(msg.format(args[0].value, name), context)
has_failed = True
# Check second argument
msg = "Argument 2 to NewType(...) must be a valid type"
try:
unanalyzed_type = expr_to_unanalyzed_type(args[1], self.options, self.api.is_stub_file)
except TypeTranslationError:
self.fail(msg, context)
return None, False
# We want to use our custom error message (see above), so we suppress
# the default error message for invalid types here.
old_type = get_proper_type(
self.api.anal_type(
unanalyzed_type,
report_invalid_types=False,
allow_placeholder=not self.api.is_func_scope(),
)
)
should_defer = False
if isinstance(old_type, PlaceholderType):
old_type = None
if old_type is None:
should_defer = True
# The caller of this function assumes that if we return a Type, it's always
# a valid one. So, we translate AnyTypes created from errors into None.
if isinstance(old_type, AnyType) and old_type.is_from_error:
self.fail(msg, context)
return None, False
return None if has_failed else old_type, should_defer
def build_newtype_typeinfo(
self,
name: str,
old_type: Type,
base_type: Instance,
line: int,
existing_info: TypeInfo | None,
) -> TypeInfo:
info = existing_info or self.api.basic_new_typeinfo(name, base_type, line)
info.bases = [base_type] # Update in case there were nested placeholders.
info.is_newtype = True
# Add __init__ method
args = [
Argument(Var("self"), NoneType(), None, ARG_POS),
self.make_argument("item", old_type),
]
signature = CallableType(
arg_types=[Instance(info, []), old_type],
arg_kinds=[arg.kind for arg in args],
arg_names=["self", "item"],
ret_type=NoneType(),
fallback=self.api.named_type("builtins.function"),
name=name,
)
init_func = FuncDef("__init__", args, Block([]), typ=signature)
init_func.info = info
init_func._fullname = info.fullname + ".__init__"
if not existing_info:
updated = True
else:
previous_sym = info.names["__init__"].node
assert isinstance(previous_sym, FuncDef)
updated = old_type != previous_sym.arguments[1].variable.type
info.names["__init__"] = SymbolTableNode(MDEF, init_func)
if has_placeholder(old_type):
self.api.process_placeholder(None, "NewType base", info, force_progress=updated)
return info
# Helpers
def make_argument(self, name: str, type: Type) -> Argument:
return Argument(Var(name), type, None, ARG_POS)
def fail(self, msg: str, ctx: Context, *, code: ErrorCode | None = None) -> None:
self.api.fail(msg, ctx, code=code)