Skip to content

New semantic analyzer: Support builtin typing aliases #6358

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

Merged
merged 23 commits into from
Feb 12, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions mypy/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -2558,6 +2558,11 @@ def process_graph(graph: Graph, manager: BuildManager) -> None:
if 'builtins' in ascc:
scc.remove('builtins')
scc.append('builtins')
# HACK: similar is needed for 'typing', for untangling the builtins SCC when new semantic
# analyzer is used.
if 'typing' in ascc:
scc.remove('typing')
scc.insert(0, 'typing')
if manager.options.verbosity >= 2:
for id in scc:
manager.trace("Priorities for %s:" % id,
Expand Down
29 changes: 25 additions & 4 deletions mypy/newsemanal/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,20 @@ def prepare_file(self, file_node: MypyFile) -> None:
self.modules['builtins'])
if file_node.fullname() == 'builtins':
self.prepare_builtins_namespace(file_node)
if file_node.fullname() == 'typing':
self.prepare_typing_namespace(file_node)

def prepare_typing_namespace(self, file_node: MypyFile) -> None:
"""Remove dummy alias definitions such as List = TypeAlias(object) from typing.

They will be replaced with real aliases when corresponding targets are ready.
"""
for stmt in file_node.defs.copy():
if (isinstance(stmt, AssignmentStmt) and len(stmt.lvalues) == 1 and
isinstance(stmt.lvalues[0], NameExpr)):
# Assignment to a simple name, remove it if it is a dummy alias.
if 'typing.' + stmt.lvalues[0].name in type_aliases:
file_node.defs.remove(stmt)

def prepare_builtins_namespace(self, file_node: MypyFile) -> None:
names = file_node.names
Expand Down Expand Up @@ -2531,10 +2545,17 @@ def process_typevar_declaration(self, s: AssignmentStmt) -> None:
assert node is not None
assert node.fullname is not None
node.kind = self.current_symbol_kind()
type_var = TypeVarExpr(name, node.fullname, values, upper_bound, variance)
type_var.line = call.line
call.analyzed = type_var
node.node = type_var
if isinstance(node.node, TypeVarExpr):
# Existing definition from previous semanal iteration, use it.
type_var = node.node
type_var.values = values
type_var.upper_bound = upper_bound
type_var.variance = variance
else:
type_var = TypeVarExpr(name, node.fullname, values, upper_bound, variance)
type_var.line = call.line
call.analyzed = type_var
node.node = type_var

def check_typevar_name(self, call: CallExpr, name: str, context: Context) -> bool:
name = unmangle(name)
Expand Down
8 changes: 8 additions & 0 deletions mypy/newsemanal/semanal_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
# Perform up to this many semantic analysis iterations until giving up trying to bind all names.
MAX_ITERATIONS = 10

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


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

worklist = scc[:]
# HACK: process core stuff first. This is mostly needed to support defining
# named tuples in builtin SCC.
if all(m in worklist for m in core_modules):
worklist += list(reversed(core_modules)) * CORE_WARMUP
iteration = 0
final_iteration = False
while worklist:
Expand Down
9 changes: 9 additions & 0 deletions mypy/newsemanal/semanal_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def lookup_qualified(self, name: str, ctx: Context,
def lookup_fully_qualified(self, name: str) -> SymbolTableNode:
raise NotImplementedError

@abstractmethod
def lookup_fully_qualified_or_none(self, name: str) -> Optional[SymbolTableNode]:
raise NotImplementedError

@abstractmethod
def fail(self, msg: str, ctx: Context, serious: bool = False, *,
blocker: bool = False) -> None:
Expand All @@ -64,6 +68,11 @@ def record_incomplete_ref(self) -> None:
def defer(self) -> None:
raise NotImplementedError

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


@trait
class SemanticAnalyzerInterface(SemanticAnalyzerCoreInterface):
Expand Down
12 changes: 11 additions & 1 deletion mypy/newsemanal/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ def try_analyze_special_unbound_type(self, t: UnboundType, fullname: str) -> Opt
" in a variable annotation", t)
return AnyType(TypeOfAny.from_error)
elif fullname == 'typing.Tuple':
# Tuple is special because it is involved in builtin import cycle
# and may be not ready when used.
sym = self.api.lookup_fully_qualified_or_none('builtins.tuple')
if not sym or isinstance(sym.node, PlaceholderNode):
Copy link
Collaborator

Choose a reason for hiding this comment

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

What happens if we don't use the correct fixture that defined tuple? Will this still generate a reasonable error message?

Copy link
Member Author

Choose a reason for hiding this comment

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

I fixed this to report a reasonable error, and added a test.

if self.api.is_incomplete_namespace('builtins'):
self.api.record_incomplete_ref()
else:
self.fail("Name 'tuple' is not defined", t)
return AnyType(TypeOfAny.special_form)
if len(t.args) == 0 and not t.empty_tuple_index:
# Bare 'Tuple' is same as 'tuple'
if self.options.disallow_any_generics and not self.is_typeshed_stub:
Expand Down Expand Up @@ -350,7 +359,8 @@ def analyze_type_with_type_info(self, info: TypeInfo, args: List[Type], ctx: Con
# Instance with an invalid number of type arguments.
instance = Instance(info, self.anal_array(args), ctx.line, ctx.column)
# Check type argument count.
if len(instance.args) != len(info.type_vars):
# TODO: remove this from here and replace with a proper separate pass.
if len(instance.args) != len(info.type_vars) and not self.defining_alias:
fix_instance(instance, self.fail)
if not args and self.options.disallow_any_generics and not self.defining_alias:
# We report/patch invalid built-in instances already during second pass.
Expand Down
1 change: 0 additions & 1 deletion mypy/test/hacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
'check-flags.test',
'check-functions.test',
'check-generics.test',
'check-ignore.test',
'check-incomplete-fixture.test',
'check-incremental.test',
'check-inference-context.test',
Expand Down
5 changes: 5 additions & 0 deletions mypy/test/testpythoneval.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None
'--no-strict-optional',
'--no-silence-site-packages',
]
if testcase.name.lower().endswith('_newsemanal'):
mypy_cmdline.append('--new-semantic-analyzer')
py2 = testcase.name.lower().endswith('python2')
if py2:
mypy_cmdline.append('--py2')
Expand Down Expand Up @@ -92,6 +94,9 @@ def test_python_evaluation(testcase: DataDrivenTestCase, cache_dir: str) -> None
output.extend(interp_out)
# Remove temp file.
os.remove(program_path)
for i, line in enumerate(output):
if os.path.sep + 'typeshed' + os.path.sep in line:
output[i] = line.split(os.path.sep)[-1]
assert_string_arrays_equal(adapt_output(testcase), output,
'Invalid output ({}, line {})'.format(
testcase.file, testcase.line))
Expand Down
6 changes: 6 additions & 0 deletions test-data/unit/check-newsemanal.test
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,12 @@ class C: pass
import a
from b import C

[case testNewAnalyzerIncompleteFixture]
from typing import Tuple

x: Tuple[int] # E: Name 'tuple' is not defined
[builtins fixtures/complex.pyi]

[case testNewAnalyzerMetaclass1]
class A(metaclass=B):
pass
Expand Down
1 change: 1 addition & 0 deletions test-data/unit/fixtures/complex.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Builtins stub used for some float/complex test cases.
# Please don't add tuple to this file, it is used to test incomplete fixtures.

class object:
def __init__(self): pass
Expand Down
11 changes: 11 additions & 0 deletions test-data/unit/pythoneval.test
Original file line number Diff line number Diff line change
Expand Up @@ -1358,3 +1358,14 @@ x = X(a=1, b='s')

[out]
_testNamedTupleNew.py:12: error: Revealed type is 'Tuple[builtins.int, fallback=_testNamedTupleNew.Child]'

[case testNewAnalyzerBasicTypeshed_newsemanal]
from typing import Dict, List, Tuple

x: Dict[str, List[int]]
reveal_type(x['test'][0])
[out]
typing.pyi: error: Class typing.Sequence has abstract attributes "__len__"
typing.pyi: note: If it is meant to be abstract, add 'abc.ABCMeta' as an explicit metaclass
builtins.pyi:39: error: Name '__class__' already defined on line 39
_testNewAnalyzerBasicTypeshed_newsemanal.py:4: error: Revealed type is 'builtins.int*'