Skip to content

Commit 2ef066f

Browse files
authored
New semantic analyzer: Support builtin typing aliases (#6358)
Fixes #6297 This adds support for real typeshed stubs that define dummy aliases like `typing.List`, `typing.Dict`, etc. This also fixes couple related issues, so that builtin SCC is almost clean (the two remaining errors are #6295 and #6357). Most notably, this PR introduces some re-ordering of targets in builtin SCC, removing this reordering requires some non-trivial work (namely #6356, #6355, and deferring targets from `named_type()`).
1 parent 1c98d01 commit 2ef066f

File tree

10 files changed

+81
-6
lines changed

10 files changed

+81
-6
lines changed

mypy/build.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2584,6 +2584,11 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
25842584
if 'builtins' in ascc:
25852585
scc.remove('builtins')
25862586
scc.append('builtins')
2587+
# HACK: similar is needed for 'typing', for untangling the builtins SCC when new semantic
2588+
# analyzer is used.
2589+
if 'typing' in ascc:
2590+
scc.remove('typing')
2591+
scc.insert(0, 'typing')
25872592
if manager.options.verbosity >= 2:
25882593
for id in scc:
25892594
manager.trace("Priorities for %s:" % id,

mypy/newsemanal/semanal.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,20 @@ def prepare_file(self, file_node: MypyFile) -> None:
348348
self.modules['builtins'])
349349
if file_node.fullname() == 'builtins':
350350
self.prepare_builtins_namespace(file_node)
351+
if file_node.fullname() == 'typing':
352+
self.prepare_typing_namespace(file_node)
353+
354+
def prepare_typing_namespace(self, file_node: MypyFile) -> None:
355+
"""Remove dummy alias definitions such as List = TypeAlias(object) from typing.
356+
357+
They will be replaced with real aliases when corresponding targets are ready.
358+
"""
359+
for stmt in file_node.defs.copy():
360+
if (isinstance(stmt, AssignmentStmt) and len(stmt.lvalues) == 1 and
361+
isinstance(stmt.lvalues[0], NameExpr)):
362+
# Assignment to a simple name, remove it if it is a dummy alias.
363+
if 'typing.' + stmt.lvalues[0].name in type_aliases:
364+
file_node.defs.remove(stmt)
351365

352366
def prepare_builtins_namespace(self, file_node: MypyFile) -> None:
353367
names = file_node.names
@@ -2531,10 +2545,17 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> None:
25312545
assert node is not None
25322546
assert node.fullname is not None
25332547
node.kind = self.current_symbol_kind()
2534-
type_var = TypeVarExpr(name, node.fullname, values, upper_bound, variance)
2535-
type_var.line = call.line
2536-
call.analyzed = type_var
2537-
node.node = type_var
2548+
if isinstance(node.node, TypeVarExpr):
2549+
# Existing definition from previous semanal iteration, use it.
2550+
type_var = node.node
2551+
type_var.values = values
2552+
type_var.upper_bound = upper_bound
2553+
type_var.variance = variance
2554+
else:
2555+
type_var = TypeVarExpr(name, node.fullname, values, upper_bound, variance)
2556+
type_var.line = call.line
2557+
call.analyzed = type_var
2558+
node.node = type_var
25382559

25392560
def check_typevar_name(self, call: CallExpr, name: str, context: Context) -> bool:
25402561
name = unmangle(name)

mypy/newsemanal/semanal_main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
# Perform up to this many semantic analysis iterations until giving up trying to bind all names.
4040
MAX_ITERATIONS = 10
4141

42+
# Number of passes over core modules before going on to the rest of the builtin SCC.
43+
CORE_WARMUP = 2
44+
core_modules = ['typing', 'builtins', 'abc', 'collections']
45+
4246

4347
def semantic_analysis_for_scc(graph: 'Graph', scc: List[str]) -> None:
4448
"""Perform semantic analysis for all modules in a SCC (import cycle).
@@ -65,6 +69,10 @@ def process_top_levels(graph: 'Graph', scc: List[str]) -> None:
6569
state.manager.incomplete_namespaces.update(scc)
6670

6771
worklist = scc[:]
72+
# HACK: process core stuff first. This is mostly needed to support defining
73+
# named tuples in builtin SCC.
74+
if all(m in worklist for m in core_modules):
75+
worklist += list(reversed(core_modules)) * CORE_WARMUP
6876
iteration = 0
6977
final_iteration = False
7078
while worklist:

mypy/newsemanal/semanal_shared.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ def lookup_qualified(self, name: str, ctx: Context,
4242
def lookup_fully_qualified(self, name: str) -> SymbolTableNode:
4343
raise NotImplementedError
4444

45+
@abstractmethod
46+
def lookup_fully_qualified_or_none(self, name: str) -> Optional[SymbolTableNode]:
47+
raise NotImplementedError
48+
4549
@abstractmethod
4650
def fail(self, msg: str, ctx: Context, serious: bool = False, *,
4751
blocker: bool = False) -> None:
@@ -64,6 +68,11 @@ def record_incomplete_ref(self) -> None:
6468
def defer(self) -> None:
6569
raise NotImplementedError
6670

71+
@abstractmethod
72+
def is_incomplete_namespace(self, fullname: str) -> bool:
73+
"""Is a module or class namespace potentially missing some definitions?"""
74+
raise NotImplementedError
75+
6776

6877
@trait
6978
class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface):

mypy/newsemanal/typeanal.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,15 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
285285
" in a variable annotation", t)
286286
return AnyType(TypeOfAny.from_error)
287287
elif fullname == 'typing.Tuple':
288+
# Tuple is special because it is involved in builtin import cycle
289+
# and may be not ready when used.
290+
sym = self.api.lookup_fully_qualified_or_none('builtins.tuple')
291+
if not sym or isinstance(sym.node, PlaceholderNode):
292+
if self.api.is_incomplete_namespace('builtins'):
293+
self.api.record_incomplete_ref()
294+
else:
295+
self.fail("Name 'tuple' is not defined", t)
296+
return AnyType(TypeOfAny.special_form)
288297
if len(t.args) == 0 and not t.empty_tuple_index:
289298
# Bare 'Tuple' is same as 'tuple'
290299
if self.options.disallow_any_generics and not self.is_typeshed_stub:
@@ -350,7 +359,8 @@ def analyze_type_with_type_info(self, info: TypeInfo, args: List[Type], ctx: Con
350359
# Instance with an invalid number of type arguments.
351360
instance = Instance(info, self.anal_array(args), ctx.line, ctx.column)
352361
# Check type argument count.
353-
if len(instance.args) != len(info.type_vars):
362+
# TODO: remove this from here and replace with a proper separate pass.
363+
if len(instance.args) != len(info.type_vars) and not self.defining_alias:
354364
fix_instance(instance, self.fail)
355365
if not args and self.options.disallow_any_generics and not self.defining_alias:
356366
# We report/patch invalid built-in instances already during second pass.

mypy/test/hacks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
'check-flags.test',
2727
'check-functions.test',
2828
'check-generics.test',
29-
'check-ignore.test',
3029
'check-incomplete-fixture.test',
3130
'check-incremental.test',
3231
'check-inference-context.test',

mypy/test/testpythoneval.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None
5656
'--no-strict-optional',
5757
'--no-silence-site-packages',
5858
]
59+
if testcase.name.lower().endswith('_newsemanal'):
60+
mypy_cmdline.append('--new-semantic-analyzer')
5961
py2 = testcase.name.lower().endswith('python2')
6062
if py2:
6163
mypy_cmdline.append('--py2')
@@ -92,6 +94,9 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None
9294
output.extend(interp_out)
9395
# Remove temp file.
9496
os.remove(program_path)
97+
for i, line in enumerate(output):
98+
if os.path.sep + 'typeshed' + os.path.sep in line:
99+
output[i] = line.split(os.path.sep)[-1]
95100
assert_string_arrays_equal(adapt_output(testcase), output,
96101
'Invalid output ({}, line {})'.format(
97102
testcase.file, testcase.line))

test-data/unit/check-newsemanal.test

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,12 @@ class C: pass
899899
import a
900900
from b import C
901901

902+
[case testNewAnalyzerIncompleteFixture]
903+
from typing import Tuple
904+
905+
x: Tuple[int] # E: Name 'tuple' is not defined
906+
[builtins fixtures/complex.pyi]
907+
902908
[case testNewAnalyzerMetaclass1]
903909
class A(metaclass=B):
904910
pass

test-data/unit/fixtures/complex.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Builtins stub used for some float/complex test cases.
2+
# Please don't add tuple to this file, it is used to test incomplete fixtures.
23

34
class object:
45
def __init__(self): pass

test-data/unit/pythoneval.test

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,3 +1358,14 @@ x = X(a=1, b='s')
13581358

13591359
[out]
13601360
_testNamedTupleNew.py:12: error: Revealed type is 'Tuple[builtins.int, fallback=_testNamedTupleNew.Child]'
1361+
1362+
[case testNewAnalyzerBasicTypeshed_newsemanal]
1363+
from typing import Dict, List, Tuple
1364+
1365+
x: Dict[str, List[int]]
1366+
reveal_type(x['test'][0])
1367+
[out]
1368+
typing.pyi: error: Class typing.Sequence has abstract attributes "__len__"
1369+
typing.pyi: note: If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass
1370+
builtins.pyi:39: error: Name '__class__' already defined on line 39
1371+
_testNewAnalyzerBasicTypeshed_newsemanal.py:4: error: Revealed type is 'builtins.int*'

0 commit comments

Comments
 (0)