Skip to content

Commit 33d3df5

Browse files
authored
Report precision stats for imports and add new precision report (#7254)
Previously when reporting per-line type checking precision, imports were treated as empty lines, which seems inconsistent. Now treat an import as precise if the target module exists, and as imprecise if the target module is missing from the build. Also add a new report, `lineprecision`, which is mostly designed to be used in tests. The output is more compact and cleaner than the existing XML-based reports. Make it possible to run report generation tests without using full stubs, since using full stubs is slow. This is still not quite perfect. If using module-level `__getattr__`, the import should probably be treated as imprecise. Also, if a missing name is ignored using # type: ignore, we may want to treat that as imprecise. For multi-line imports, we only report precision for the first line. These would be a bit tricky to fix in the report generator so I decided to skip these for now. [Also fixes a small bug with % interpolation that I encountered.]
1 parent 455e600 commit 33d3df5

11 files changed

+381
-83
lines changed

mypy/build.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ def report_file(self,
748748
type_map: Dict[Expression, Type],
749749
options: Options) -> None:
750750
if self.reports is not None and self.source_set.is_source(file):
751-
self.reports.file(file, type_map, options)
751+
self.reports.file(file, self.modules, type_map, options)
752752

753753
def verbosity(self) -> int:
754754
return self.options.verbosity
@@ -2120,7 +2120,7 @@ def semantic_analysis_pass_three(self) -> None:
21202120
self.manager.semantic_analyzer_pass3.visit_file(self.tree, self.xpath,
21212121
self.options, patches)
21222122
if self.options.dump_type_stats:
2123-
dump_type_stats(self.tree, self.xpath)
2123+
dump_type_stats(self.tree, self.xpath, self.manager.modules)
21242124
self.patches = patches + self.patches
21252125

21262126
def semantic_analysis_apply_patches(self) -> None:
@@ -2165,7 +2165,10 @@ def finish_passes(self) -> None:
21652165
self._patch_indirect_dependencies(self.type_checker().module_refs, self.type_map())
21662166

21672167
if self.options.dump_inference_stats:
2168-
dump_type_stats(self.tree, self.xpath, inferred=True,
2168+
dump_type_stats(self.tree,
2169+
self.xpath,
2170+
modules=self.manager.modules,
2171+
inferred=True,
21692172
typemap=self.type_map())
21702173
manager.report_file(self.tree, self.type_map(), self.options)
21712174

mypy/checkstrformat.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ def check_simple_str_interpolation(self, specifiers: List[ConversionSpecifier],
143143
rep_types = rhs_type.items
144144
elif isinstance(rhs_type, AnyType):
145145
return
146+
elif isinstance(rhs_type, Instance) and rhs_type.type.fullname() == 'builtins.tuple':
147+
# Assume that an arbitrary-length tuple has the right number of items.
148+
rep_types = [rhs_type.args[0]] * len(checkers)
146149
else:
147150
rep_types = [rhs_type]
148151

mypy/defaults.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@
2626
'xslt-html',
2727
'xslt-txt',
2828
'html',
29-
'txt'] # type: Final
29+
'txt',
30+
'lineprecision'] # type: Final

mypy/report.py

Lines changed: 136 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from urllib.request import pathname2url
1414

1515
import typing
16-
from typing import Any, Callable, Dict, List, Optional, Tuple, cast
16+
from typing import Any, Callable, Dict, List, Optional, Tuple, cast, Iterator
1717
from typing_extensions import Final
1818

1919
from mypy.nodes import MypyFile, Expression, FuncDef
@@ -74,9 +74,13 @@ def add_report(self, report_type: str, report_dir: str) -> 'AbstractReporter':
7474
self.named_reporters[report_type] = reporter
7575
return reporter
7676

77-
def file(self, tree: MypyFile, type_map: Dict[Expression, Type], options: Options) -> None:
77+
def file(self,
78+
tree: MypyFile,
79+
modules: Dict[str, MypyFile],
80+
type_map: Dict[Expression, Type],
81+
options: Options) -> None:
7882
for reporter in self.reporters:
79-
reporter.on_file(tree, type_map, options)
83+
reporter.on_file(tree, modules, type_map, options)
8084

8185
def finish(self) -> None:
8286
for reporter in self.reporters:
@@ -90,7 +94,11 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
9094
stats.ensure_dir_exists(output_dir)
9195

9296
@abstractmethod
93-
def on_file(self, tree: MypyFile, type_map: Dict[Expression, Type], options: Options) -> None:
97+
def on_file(self,
98+
tree: MypyFile,
99+
modules: Dict[str, MypyFile],
100+
type_map: Dict[Expression, Type],
101+
options: Options) -> None:
94102
pass
95103

96104
@abstractmethod
@@ -108,6 +116,23 @@ def alias_reporter(source_reporter: str, target_reporter: str) -> None:
108116
reporter_classes[target_reporter] = reporter_classes[source_reporter]
109117

110118

119+
def should_skip_path(path: str) -> bool:
120+
if stats.is_special_module(path):
121+
return True
122+
if path.startswith('..'):
123+
return True
124+
if 'stubs' in path.split('/') or 'stubs' in path.split(os.sep):
125+
return True
126+
return False
127+
128+
129+
def iterate_python_lines(path: str) -> Iterator[Tuple[int, str]]:
130+
"""Return an iterator over (line number, line text) from a Python file."""
131+
with tokenize.open(path) as input_file:
132+
for line_info in enumerate(input_file, 1):
133+
yield line_info
134+
135+
111136
class FuncCounterVisitor(TraverserVisitor):
112137
def __init__(self) -> None:
113138
super().__init__()
@@ -124,6 +149,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
124149

125150
def on_file(self,
126151
tree: MypyFile,
152+
modules: Dict[str, MypyFile],
127153
type_map: Dict[Expression, Type],
128154
options: Options) -> None:
129155
# Count physical lines. This assumes the file's encoding is a
@@ -162,17 +188,23 @@ def on_finish(self) -> None:
162188

163189

164190
class AnyExpressionsReporter(AbstractReporter):
191+
"""Report frequencies of different kinds of Any types."""
192+
165193
def __init__(self, reports: Reports, output_dir: str) -> None:
166194
super().__init__(reports, output_dir)
167195
self.counts = {} # type: Dict[str, Tuple[int, int]]
168196
self.any_types_counter = {} # type: Dict[str, typing.Counter[int]]
169197

170198
def on_file(self,
171199
tree: MypyFile,
200+
modules: Dict[str, MypyFile],
172201
type_map: Dict[Expression, Type],
173202
options: Options) -> None:
174-
visitor = stats.StatisticsVisitor(inferred=True, filename=tree.fullname(),
175-
typemap=type_map, all_nodes=True,
203+
visitor = stats.StatisticsVisitor(inferred=True,
204+
filename=tree.fullname(),
205+
modules=modules,
206+
typemap=type_map,
207+
all_nodes=True,
176208
visit_untyped_defs=False)
177209
tree.accept(visitor)
178210
self.any_types_counter[tree.fullname()] = visitor.type_of_any_counter
@@ -354,12 +386,14 @@ class LineCoverageReporter(AbstractReporter):
354386
source file's absolute pathname the list of line numbers that
355387
belong to typed functions in that file.
356388
"""
389+
357390
def __init__(self, reports: Reports, output_dir: str) -> None:
358391
super().__init__(reports, output_dir)
359392
self.lines_covered = {} # type: Dict[str, List[int]]
360393

361394
def on_file(self,
362395
tree: MypyFile,
396+
modules: Dict[str, MypyFile],
363397
type_map: Dict[Expression, Type],
364398
options: Options) -> None:
365399
with open(tree.path) as f:
@@ -421,34 +455,33 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
421455

422456
def on_file(self,
423457
tree: MypyFile,
458+
modules: Dict[str, MypyFile],
424459
type_map: Dict[Expression, Type],
425460
options: Options) -> None:
426461
self.last_xml = None
427462
path = os.path.relpath(tree.path)
428-
if stats.is_special_module(path):
429-
return
430-
if path.startswith('..'):
431-
return
432-
if 'stubs' in path.split('/'):
463+
if should_skip_path(path):
433464
return
434465

435-
visitor = stats.StatisticsVisitor(inferred=True, filename=tree.fullname(),
436-
typemap=type_map, all_nodes=True)
466+
visitor = stats.StatisticsVisitor(inferred=True,
467+
filename=tree.fullname(),
468+
modules=modules,
469+
typemap=type_map,
470+
all_nodes=True)
437471
tree.accept(visitor)
438472

439473
root = etree.Element('mypy-report-file', name=path, module=tree._fullname)
440474
doc = etree.ElementTree(root)
441475
file_info = FileInfo(path, tree._fullname)
442476

443-
with tokenize.open(path) as input_file:
444-
for lineno, line_text in enumerate(input_file, 1):
445-
status = visitor.line_map.get(lineno, stats.TYPE_EMPTY)
446-
file_info.counts[status] += 1
447-
etree.SubElement(root, 'line',
448-
number=str(lineno),
449-
precision=stats.precision_names[status],
450-
content=line_text.rstrip('\n').translate(self.control_fixer),
451-
any_info=self._get_any_info_for_line(visitor, lineno))
477+
for lineno, line_text in iterate_python_lines(path):
478+
status = visitor.line_map.get(lineno, stats.TYPE_EMPTY)
479+
file_info.counts[status] += 1
480+
etree.SubElement(root, 'line',
481+
number=str(lineno),
482+
precision=stats.precision_names[status],
483+
content=line_text.rstrip('\n').translate(self.control_fixer),
484+
any_info=self._get_any_info_for_line(visitor, lineno))
452485
# Assumes a layout similar to what XmlReporter uses.
453486
xslt_path = os.path.relpath('mypy-html.xslt', path)
454487
transform_pi = etree.ProcessingInstruction('xml-stylesheet',
@@ -506,8 +539,8 @@ def get_line_rate(covered_lines: int, total_lines: int) -> str:
506539

507540

508541
class CoberturaPackage(object):
509-
"""Container for XML and statistics mapping python modules to Cobertura package
510-
"""
542+
"""Container for XML and statistics mapping python modules to Cobertura package."""
543+
511544
def __init__(self, name: str) -> None:
512545
self.name = name
513546
self.classes = {} # type: Dict[str, Any]
@@ -535,8 +568,7 @@ def add_packages(self, parent_element: Any) -> None:
535568

536569

537570
class CoberturaXmlReporter(AbstractReporter):
538-
"""Reporter for generating Cobertura compliant XML.
539-
"""
571+
"""Reporter for generating Cobertura compliant XML."""
540572

541573
def __init__(self, reports: Reports, output_dir: str) -> None:
542574
super().__init__(reports, output_dir)
@@ -549,11 +581,15 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
549581

550582
def on_file(self,
551583
tree: MypyFile,
584+
modules: Dict[str, MypyFile],
552585
type_map: Dict[Expression, Type],
553586
options: Options) -> None:
554587
path = os.path.relpath(tree.path)
555-
visitor = stats.StatisticsVisitor(inferred=True, filename=tree.fullname(),
556-
typemap=type_map, all_nodes=True)
588+
visitor = stats.StatisticsVisitor(inferred=True,
589+
filename=tree.fullname(),
590+
modules=modules,
591+
typemap=type_map,
592+
all_nodes=True)
557593
tree.accept(visitor)
558594

559595
class_name = os.path.basename(path)
@@ -646,6 +682,7 @@ class XmlReporter(AbstractXmlReporter):
646682

647683
def on_file(self,
648684
tree: MypyFile,
685+
modules: Dict[str, MypyFile],
649686
type_map: Dict[Expression, Type],
650687
options: Options) -> None:
651688
last_xml = self.memory_xml.last_xml
@@ -688,6 +725,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
688725

689726
def on_file(self,
690727
tree: MypyFile,
728+
modules: Dict[str, MypyFile],
691729
type_map: Dict[Expression, Type],
692730
options: Options) -> None:
693731
last_xml = self.memory_xml.last_xml
@@ -730,6 +768,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
730768

731769
def on_file(self,
732770
tree: MypyFile,
771+
modules: Dict[str, MypyFile],
733772
type_map: Dict[Expression, Type],
734773
options: Options) -> None:
735774
pass
@@ -749,6 +788,75 @@ def on_finish(self) -> None:
749788
alias_reporter('xslt-html', 'html')
750789
alias_reporter('xslt-txt', 'txt')
751790

791+
792+
class LinePrecisionReporter(AbstractReporter):
793+
"""Report per-module line counts for typing precision.
794+
795+
Each line is classified into one of these categories:
796+
797+
* precise (fully type checked)
798+
* imprecise (Any types in a type component, such as List[Any])
799+
* any (something with an Any type, implicit or explicit)
800+
* empty (empty line, comment or docstring)
801+
* unanalyzed (mypy considers line unreachable)
802+
803+
The meaning of these categories varies slightly depending on
804+
context.
805+
"""
806+
807+
def __init__(self, reports: Reports, output_dir: str) -> None:
808+
super().__init__(reports, output_dir)
809+
self.files = [] # type: List[FileInfo]
810+
811+
def on_file(self,
812+
tree: MypyFile,
813+
modules: Dict[str, MypyFile],
814+
type_map: Dict[Expression, Type],
815+
options: Options) -> None:
816+
path = os.path.relpath(tree.path)
817+
if should_skip_path(path):
818+
return
819+
820+
visitor = stats.StatisticsVisitor(inferred=True,
821+
filename=tree.fullname(),
822+
modules=modules,
823+
typemap=type_map,
824+
all_nodes=True)
825+
tree.accept(visitor)
826+
827+
file_info = FileInfo(path, tree._fullname)
828+
for lineno, _ in iterate_python_lines(path):
829+
status = visitor.line_map.get(lineno, stats.TYPE_EMPTY)
830+
file_info.counts[status] += 1
831+
832+
self.files.append(file_info)
833+
834+
def on_finish(self) -> None:
835+
output_files = sorted(self.files, key=lambda x: x.module)
836+
report_file = os.path.join(self.output_dir, 'lineprecision.txt')
837+
width = max(4, max(len(info.module) for info in output_files))
838+
titles = ('Lines', 'Precise', 'Imprecise', 'Any', 'Empty', 'Unanalyzed')
839+
widths = (width,) + tuple(len(t) for t in titles)
840+
# TODO: Need mypyc mypy pin move
841+
fmt = '{:%d} {:%d} {:%d} {:%d} {:%d} {:%d} {:%d}\n' % widths # type: ignore
842+
with open(report_file, 'w') as f:
843+
f.write(
844+
fmt.format('Name', *titles))
845+
f.write('-' * (width + 51) + '\n')
846+
for file_info in output_files:
847+
counts = file_info.counts
848+
f.write(fmt.format(file_info.module.ljust(width),
849+
file_info.total(),
850+
counts[stats.TYPE_PRECISE],
851+
counts[stats.TYPE_IMPRECISE],
852+
counts[stats.TYPE_ANY],
853+
counts[stats.TYPE_EMPTY],
854+
counts[stats.TYPE_UNANALYZED]))
855+
856+
857+
register_reporter('lineprecision', LinePrecisionReporter)
858+
859+
752860
# Reporter class names are defined twice to speed up mypy startup, as this
753861
# module is slow to import. Ensure that the two definitions match.
754862
assert set(reporter_classes) == set(REPORTER_NAMES)

0 commit comments

Comments
 (0)