Skip to content

Commit 53ebb6b

Browse files
authored
Various improvements to fine-grained incremental checking (#4423)
This includes several fixes to fine-grained incremental mode. These are the main ones: 1) Support --ignore-missing-imports in fine-grained incremental mode Most of the changes are about supporting command-line options in fine-grained incremental test cases. 2) Add test case for skipping imports in fine-grained incremental mode Most of the changes just enable testing skipped imports. 3) Fix unannotated functions in fine-grained incremental mode 4) Fix super() in fine-grained incremental mode 5) Fix AST diff of overloaded methods and properties with setters
1 parent bab5fb6 commit 53ebb6b

11 files changed

+314
-45
lines changed

mypy/semanal.py

+2
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,8 @@ def visit_overloaded_func_def(self, defn: OverloadedFuncDef) -> None:
480480
first_item.is_overload = True
481481
first_item.accept(self)
482482

483+
defn._fullname = self.qualified_name(defn.name())
484+
483485
if isinstance(first_item, Decorator) and first_item.func.is_property:
484486
first_item.func.is_overload = True
485487
self.analyze_property_with_multi_part_definition(defn)

mypy/server/astdiff.py

+26-6
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
Only look at detail at definitions at the current module.
88
"""
99

10-
from typing import Set, List, TypeVar, Dict, Tuple, Optional, Sequence
10+
from typing import Set, List, TypeVar, Dict, Tuple, Optional, Sequence, Union
1111

1212
from mypy.nodes import (
13-
SymbolTable, SymbolTableNode, FuncBase, TypeInfo, Var, MypyFile, SymbolNode, Decorator,
14-
TypeVarExpr, MODULE_REF, TYPE_ALIAS, UNBOUND_IMPORTED, TVAR
13+
SymbolTable, SymbolTableNode, TypeInfo, Var, MypyFile, SymbolNode, Decorator, TypeVarExpr,
14+
OverloadedFuncDef, FuncItem, MODULE_REF, TYPE_ALIAS, UNBOUND_IMPORTED, TVAR
1515
)
1616
from mypy.types import (
1717
Type, TypeVisitor, UnboundType, TypeList, AnyType, NoneTyp, UninhabitedType,
1818
ErasedType, DeletedType, Instance, TypeVarType, CallableType, TupleType, TypedDictType,
19-
UnionType, Overloaded, PartialType, TypeType
19+
UnionType, Overloaded, PartialType, TypeType, function_type
2020
)
2121
from mypy.util import get_prefix
2222

@@ -232,9 +232,13 @@ def snapshot_definition(node: Optional[SymbolNode],
232232
The representation is nested tuples and dicts. Only externally
233233
visible attributes are included.
234234
"""
235-
if isinstance(node, FuncBase):
235+
if isinstance(node, (OverloadedFuncDef, FuncItem)):
236236
# TODO: info
237-
return ('Func', common, node.is_property, snapshot_type(node.type))
237+
if node.type:
238+
signature = snapshot_type(node.type)
239+
else:
240+
signature = snapshot_untyped_signature(node)
241+
return ('Func', common, node.is_property, signature)
238242
elif isinstance(node, Var):
239243
return ('Var', common, snapshot_optional_type(node.type))
240244
elif isinstance(node, Decorator):
@@ -373,3 +377,19 @@ def visit_partial_type(self, typ: PartialType) -> SnapshotItem:
373377

374378
def visit_type_type(self, typ: TypeType) -> SnapshotItem:
375379
return ('TypeType', snapshot_type(typ.item))
380+
381+
382+
def snapshot_untyped_signature(func: Union[OverloadedFuncDef, FuncItem]) -> Tuple[object, ...]:
383+
if isinstance(func, FuncItem):
384+
return (tuple(func.arg_names), tuple(func.arg_kinds))
385+
else:
386+
result = []
387+
for item in func.items:
388+
if isinstance(item, Decorator):
389+
if item.var.type:
390+
result.append(snapshot_type(item.var.type))
391+
else:
392+
result.append(('DecoratorWithoutType',))
393+
else:
394+
result.append(snapshot_untyped_signature(item))
395+
return tuple(result)

mypy/server/astmerge.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from mypy.nodes import (
99
Node, MypyFile, SymbolTable, Block, AssignmentStmt, NameExpr, MemberExpr, RefExpr, TypeInfo,
10-
FuncDef, ClassDef, NamedTupleExpr, SymbolNode, Var, Statement, MDEF
10+
FuncDef, ClassDef, NamedTupleExpr, SymbolNode, Var, Statement, SuperExpr, MDEF
1111
)
1212
from mypy.traverser import TraverserVisitor
1313
from mypy.types import (
@@ -123,6 +123,10 @@ def visit_namedtuple_expr(self, node: NamedTupleExpr) -> None:
123123
super().visit_namedtuple_expr(node)
124124
self.process_type_info(node.info)
125125

126+
def visit_super_expr(self, node: SuperExpr) -> None:
127+
super().visit_super_expr(node)
128+
node.info = self.fixup(node.info)
129+
126130
# Helpers
127131

128132
def fixup(self, node: SN) -> SN:

mypy/server/update.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ def invalidate_stale_cache_entries(cache: SavedCache,
513513
def verify_dependencies(state: State, manager: BuildManager) -> None:
514514
"""Report errors for import targets in module that don't exist."""
515515
for dep in state.dependencies + state.suppressed: # TODO: ancestors?
516-
if dep not in manager.modules:
516+
if dep not in manager.modules and not manager.options.ignore_missing_imports:
517517
assert state.tree
518518
line = find_import_line(state.tree, dep) or 1
519519
assert state.path

mypy/test/helpers.py

+31
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from mypy import defaults
99
from mypy.myunit import AssertionFailure
10+
from mypy.main import process_options
11+
from mypy.options import Options
1012
from mypy.test.data import DataDrivenTestCase
1113

1214

@@ -308,3 +310,32 @@ def retry_on_error(func: Callable[[], Any], max_wait: float = 1.0) -> None:
308310
# Done enough waiting, the error seems persistent.
309311
raise
310312
time.sleep(wait_time)
313+
314+
315+
def parse_options(program_text: str, testcase: DataDrivenTestCase,
316+
incremental_step: int) -> Options:
317+
"""Parse comments like '# flags: --foo' in a test case."""
318+
options = Options()
319+
flags = re.search('# flags: (.*)$', program_text, flags=re.MULTILINE)
320+
if incremental_step > 1:
321+
flags2 = re.search('# flags{}: (.*)$'.format(incremental_step), program_text,
322+
flags=re.MULTILINE)
323+
if flags2:
324+
flags = flags2
325+
326+
flag_list = None
327+
if flags:
328+
flag_list = flags.group(1).split()
329+
targets, options = process_options(flag_list, require_targets=False)
330+
if targets:
331+
# TODO: support specifying targets via the flags pragma
332+
raise RuntimeError('Specifying targets via the flags pragma is not supported.')
333+
else:
334+
options = Options()
335+
336+
# Allow custom python version to override testcase_pyversion
337+
if (not flag_list or
338+
all(flag not in flag_list for flag in ['--python-version', '-2', '--py2'])):
339+
options.python_version = testcase_pyversion(testcase.file, testcase.name)
340+
341+
return options

mypy/test/testcheck.py

+2-30
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@
77
from typing import Dict, List, Optional, Set, Tuple
88

99
from mypy import build, defaults
10-
from mypy.main import process_options
1110
from mypy.build import BuildSource, find_module_clear_caches
1211
from mypy.myunit import AssertionFailure
1312
from mypy.test.config import test_temp_dir
1413
from mypy.test.data import DataDrivenTestCase, DataSuite
1514
from mypy.test.helpers import (
1615
assert_string_arrays_equal, normalize_error_messages,
17-
retry_on_error, testcase_pyversion, update_testcase_output,
16+
retry_on_error, update_testcase_output, parse_options
1817
)
1918
from mypy.errors import CompileError
2019
from mypy.options import Options
@@ -155,7 +154,7 @@ def run_case_once(self, testcase: DataDrivenTestCase, incremental_step: int = 0)
155154
retry_on_error(lambda: os.remove(path))
156155

157156
# Parse options after moving files (in case mypy.ini is being moved).
158-
options = self.parse_options(original_program_text, testcase, incremental_step)
157+
options = parse_options(original_program_text, testcase, incremental_step)
159158
options.use_builtins_fixtures = True
160159
options.show_traceback = True
161160
if 'optional' in testcase.file:
@@ -328,30 +327,3 @@ def parse_module(self,
328327
return out
329328
else:
330329
return [('__main__', 'main', program_text)]
331-
332-
def parse_options(self, program_text: str, testcase: DataDrivenTestCase,
333-
incremental_step: int) -> Options:
334-
options = Options()
335-
flags = re.search('# flags: (.*)$', program_text, flags=re.MULTILINE)
336-
if incremental_step > 1:
337-
flags2 = re.search('# flags{}: (.*)$'.format(incremental_step), program_text,
338-
flags=re.MULTILINE)
339-
if flags2:
340-
flags = flags2
341-
342-
flag_list = None
343-
if flags:
344-
flag_list = flags.group(1).split()
345-
targets, options = process_options(flag_list, require_targets=False)
346-
if targets:
347-
# TODO: support specifying targets via the flags pragma
348-
raise RuntimeError('Specifying targets via the flags pragma is not supported.')
349-
else:
350-
options = Options()
351-
352-
# Allow custom python version to override testcase_pyversion
353-
if (not flag_list or
354-
all(flag not in flag_list for flag in ['--python-version', '-2', '--py2'])):
355-
options.python_version = testcase_pyversion(testcase.file, testcase.name)
356-
357-
return options

mypy/test/testfinegrained.py

+46-6
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
from mypy.server.update import FineGrainedBuildManager
2424
from mypy.strconv import StrConv, indent
2525
from mypy.test.config import test_temp_dir, test_data_prefix
26-
from mypy.test.data import parse_test_cases, DataDrivenTestCase, DataSuite, UpdateFile
27-
from mypy.test.helpers import assert_string_arrays_equal
26+
from mypy.test.data import (
27+
parse_test_cases, DataDrivenTestCase, DataSuite, UpdateFile, module_from_path
28+
)
29+
from mypy.test.helpers import assert_string_arrays_equal, parse_options
2830
from mypy.test.testtypegen import ignore_node
2931
from mypy.types import TypeStrVisitor, Type
3032
from mypy.util import short_type
@@ -42,7 +44,8 @@ class FineGrainedSuite(DataSuite):
4244

4345
def run_case(self, testcase: DataDrivenTestCase) -> None:
4446
main_src = '\n'.join(testcase.input)
45-
messages, manager, graph = self.build(main_src)
47+
sources_override = self.parse_sources(main_src)
48+
messages, manager, graph = self.build(main_src, testcase, sources_override)
4649

4750
a = []
4851
if messages:
@@ -63,6 +66,10 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
6366
# Delete file
6467
os.remove(op.path)
6568
modules.append((op.module, op.path))
69+
if sources_override is not None:
70+
modules = [(module, path)
71+
for module, path in sources_override
72+
if any(m == module for m, _ in modules)]
6673
new_messages = fine_grained_manager.update(modules)
6774
all_triggered.append(fine_grained_manager.triggered)
6875
new_messages = normalize_messages(new_messages)
@@ -85,16 +92,28 @@ def run_case(self, testcase: DataDrivenTestCase) -> None:
8592
'Invalid active triggers ({}, line {})'.format(testcase.file,
8693
testcase.line))
8794

88-
def build(self, source: str) -> Tuple[List[str], BuildManager, Graph]:
89-
options = Options()
95+
def build(self,
96+
source: str,
97+
testcase: DataDrivenTestCase,
98+
sources_override: Optional[List[Tuple[str, str]]]) -> Tuple[List[str],
99+
BuildManager,
100+
Graph]:
101+
# This handles things like '# flags: --foo'.
102+
options = parse_options(source, testcase, incremental_step=1)
90103
options.incremental = True
91104
options.use_builtins_fixtures = True
92105
options.show_traceback = True
93106
main_path = os.path.join(test_temp_dir, 'main')
94107
with open(main_path, 'w') as f:
95108
f.write(source)
109+
if sources_override is not None:
110+
sources = [BuildSource(path, module, None)
111+
for module, path in sources_override]
112+
else:
113+
sources = [BuildSource(main_path, None, None)]
114+
print(sources)
96115
try:
97-
result = build.build(sources=[BuildSource(main_path, None, None)],
116+
result = build.build(sources=sources,
98117
options=options,
99118
alt_lib_path=test_temp_dir)
100119
except CompileError as e:
@@ -112,6 +131,27 @@ def format_triggered(self, triggered: List[List[str]]) -> List[str]:
112131
result.append(('%d: %s' % (n + 2, ', '.join(filtered))).strip())
113132
return result
114133

134+
def parse_sources(self, program_text: str) -> Optional[List[Tuple[str, str]]]:
135+
"""Return target (module, path) tuples for a test case, if not using the defaults.
136+
137+
These are defined through a comment like '# cmd: main a.py' in the test case
138+
description.
139+
"""
140+
# TODO: Support defining separately for each incremental step.
141+
m = re.search('# cmd: mypy ([a-zA-Z0-9_. ]+)$', program_text, flags=re.MULTILINE)
142+
if m:
143+
# The test case wants to use a non-default set of files.
144+
paths = m.group(1).strip().split()
145+
result = []
146+
for path in paths:
147+
path = os.path.join(test_temp_dir, path)
148+
module = module_from_path(path)
149+
if module == 'main':
150+
module = '__main__'
151+
result.append((module, path))
152+
return result
153+
return None
154+
115155

116156
def normalize_messages(messages: List[str]) -> List[str]:
117157
return [re.sub('^tmp' + re.escape(os.sep), '', message)

mypy/traverser.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
GeneratorExpr, ListComprehension, SetComprehension, DictionaryComprehension,
1111
ConditionalExpr, TypeApplication, ExecStmt, Import, ImportFrom,
1212
LambdaExpr, ComparisonExpr, OverloadedFuncDef, YieldFromExpr,
13-
YieldExpr, StarExpr, BackquoteExpr, AwaitExpr, PrintStmt,
13+
YieldExpr, StarExpr, BackquoteExpr, AwaitExpr, PrintStmt, SuperExpr,
1414
)
1515

1616

@@ -250,6 +250,9 @@ def visit_backquote_expr(self, o: BackquoteExpr) -> None:
250250
def visit_await_expr(self, o: AwaitExpr) -> None:
251251
o.expr.accept(self)
252252

253+
def visit_super_expr(self, o: SuperExpr) -> None:
254+
o.call.accept(self)
255+
253256
def visit_import(self, o: Import) -> None:
254257
for a in o.assignments:
255258
a.accept(self)

test-data/unit/diff.test

+73
Original file line numberDiff line numberDiff line change
@@ -476,3 +476,76 @@ def g(x: object) -> Iterator[None]:
476476
[builtins fixtures/list.pyi]
477477
[out]
478478
__main__.g
479+
480+
[case testOverloadedMethod]
481+
from typing import overload
482+
483+
class A:
484+
@overload
485+
def f(self, x: int) -> int: pass
486+
@overload
487+
def f(self, x: str) -> str: pass
488+
def f(self, x): pass
489+
490+
@overload
491+
def g(self, x: int) -> int: pass
492+
@overload
493+
def g(self, x: str) -> str: pass
494+
def g(self, x): pass
495+
[file next.py]
496+
from typing import overload
497+
498+
class A:
499+
@overload
500+
def f(self, x: int) -> int: pass
501+
@overload
502+
def f(self, x: str) -> str: pass
503+
def f(self, x): pass
504+
505+
@overload
506+
def g(self, x: int) -> int: pass
507+
@overload
508+
def g(self, x: object) -> object: pass
509+
def g(self, x): pass
510+
[out]
511+
__main__.A.g
512+
513+
[case testPropertyWithSetter]
514+
class A:
515+
@property
516+
def x(self) -> int:
517+
pass
518+
519+
@x.setter
520+
def x(self, o: int) -> None:
521+
pass
522+
523+
class B:
524+
@property
525+
def x(self) -> int:
526+
pass
527+
528+
@x.setter
529+
def x(self, o: int) -> None:
530+
pass
531+
[file next.py]
532+
class A:
533+
@property
534+
def x(self) -> int:
535+
pass
536+
537+
@x.setter
538+
def x(self, o: int) -> None:
539+
pass
540+
541+
class B:
542+
@property
543+
def x(self) -> str:
544+
pass
545+
546+
@x.setter
547+
def x(self, o: str) -> None:
548+
pass
549+
[builtins fixtures/property.pyi]
550+
[out]
551+
__main__.B.x

0 commit comments

Comments
 (0)