13
13
from urllib .request import pathname2url
14
14
15
15
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
17
17
from typing_extensions import Final
18
18
19
19
from mypy .nodes import MypyFile , Expression , FuncDef
@@ -74,9 +74,13 @@ def add_report(self, report_type: str, report_dir: str) -> 'AbstractReporter':
74
74
self .named_reporters [report_type ] = reporter
75
75
return reporter
76
76
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 :
78
82
for reporter in self .reporters :
79
- reporter .on_file (tree , type_map , options )
83
+ reporter .on_file (tree , modules , type_map , options )
80
84
81
85
def finish (self ) -> None :
82
86
for reporter in self .reporters :
@@ -90,7 +94,11 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
90
94
stats .ensure_dir_exists (output_dir )
91
95
92
96
@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 :
94
102
pass
95
103
96
104
@abstractmethod
@@ -108,6 +116,23 @@ def alias_reporter(source_reporter: str, target_reporter: str) -> None:
108
116
reporter_classes [target_reporter ] = reporter_classes [source_reporter ]
109
117
110
118
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
+
111
136
class FuncCounterVisitor (TraverserVisitor ):
112
137
def __init__ (self ) -> None :
113
138
super ().__init__ ()
@@ -124,6 +149,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
124
149
125
150
def on_file (self ,
126
151
tree : MypyFile ,
152
+ modules : Dict [str , MypyFile ],
127
153
type_map : Dict [Expression , Type ],
128
154
options : Options ) -> None :
129
155
# Count physical lines. This assumes the file's encoding is a
@@ -162,17 +188,23 @@ def on_finish(self) -> None:
162
188
163
189
164
190
class AnyExpressionsReporter (AbstractReporter ):
191
+ """Report frequencies of different kinds of Any types."""
192
+
165
193
def __init__ (self , reports : Reports , output_dir : str ) -> None :
166
194
super ().__init__ (reports , output_dir )
167
195
self .counts = {} # type: Dict[str, Tuple[int, int]]
168
196
self .any_types_counter = {} # type: Dict[str, typing.Counter[int]]
169
197
170
198
def on_file (self ,
171
199
tree : MypyFile ,
200
+ modules : Dict [str , MypyFile ],
172
201
type_map : Dict [Expression , Type ],
173
202
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 ,
176
208
visit_untyped_defs = False )
177
209
tree .accept (visitor )
178
210
self .any_types_counter [tree .fullname ()] = visitor .type_of_any_counter
@@ -354,12 +386,14 @@ class LineCoverageReporter(AbstractReporter):
354
386
source file's absolute pathname the list of line numbers that
355
387
belong to typed functions in that file.
356
388
"""
389
+
357
390
def __init__ (self , reports : Reports , output_dir : str ) -> None :
358
391
super ().__init__ (reports , output_dir )
359
392
self .lines_covered = {} # type: Dict[str, List[int]]
360
393
361
394
def on_file (self ,
362
395
tree : MypyFile ,
396
+ modules : Dict [str , MypyFile ],
363
397
type_map : Dict [Expression , Type ],
364
398
options : Options ) -> None :
365
399
with open (tree .path ) as f :
@@ -421,34 +455,33 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
421
455
422
456
def on_file (self ,
423
457
tree : MypyFile ,
458
+ modules : Dict [str , MypyFile ],
424
459
type_map : Dict [Expression , Type ],
425
460
options : Options ) -> None :
426
461
self .last_xml = None
427
462
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 ):
433
464
return
434
465
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 )
437
471
tree .accept (visitor )
438
472
439
473
root = etree .Element ('mypy-report-file' , name = path , module = tree ._fullname )
440
474
doc = etree .ElementTree (root )
441
475
file_info = FileInfo (path , tree ._fullname )
442
476
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 ))
452
485
# Assumes a layout similar to what XmlReporter uses.
453
486
xslt_path = os .path .relpath ('mypy-html.xslt' , path )
454
487
transform_pi = etree .ProcessingInstruction ('xml-stylesheet' ,
@@ -506,8 +539,8 @@ def get_line_rate(covered_lines: int, total_lines: int) -> str:
506
539
507
540
508
541
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
+
511
544
def __init__ (self , name : str ) -> None :
512
545
self .name = name
513
546
self .classes = {} # type: Dict[str, Any]
@@ -535,8 +568,7 @@ def add_packages(self, parent_element: Any) -> None:
535
568
536
569
537
570
class CoberturaXmlReporter (AbstractReporter ):
538
- """Reporter for generating Cobertura compliant XML.
539
- """
571
+ """Reporter for generating Cobertura compliant XML."""
540
572
541
573
def __init__ (self , reports : Reports , output_dir : str ) -> None :
542
574
super ().__init__ (reports , output_dir )
@@ -549,11 +581,15 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
549
581
550
582
def on_file (self ,
551
583
tree : MypyFile ,
584
+ modules : Dict [str , MypyFile ],
552
585
type_map : Dict [Expression , Type ],
553
586
options : Options ) -> None :
554
587
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 )
557
593
tree .accept (visitor )
558
594
559
595
class_name = os .path .basename (path )
@@ -646,6 +682,7 @@ class XmlReporter(AbstractXmlReporter):
646
682
647
683
def on_file (self ,
648
684
tree : MypyFile ,
685
+ modules : Dict [str , MypyFile ],
649
686
type_map : Dict [Expression , Type ],
650
687
options : Options ) -> None :
651
688
last_xml = self .memory_xml .last_xml
@@ -688,6 +725,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
688
725
689
726
def on_file (self ,
690
727
tree : MypyFile ,
728
+ modules : Dict [str , MypyFile ],
691
729
type_map : Dict [Expression , Type ],
692
730
options : Options ) -> None :
693
731
last_xml = self .memory_xml .last_xml
@@ -730,6 +768,7 @@ def __init__(self, reports: Reports, output_dir: str) -> None:
730
768
731
769
def on_file (self ,
732
770
tree : MypyFile ,
771
+ modules : Dict [str , MypyFile ],
733
772
type_map : Dict [Expression , Type ],
734
773
options : Options ) -> None :
735
774
pass
@@ -749,6 +788,75 @@ def on_finish(self) -> None:
749
788
alias_reporter ('xslt-html' , 'html' )
750
789
alias_reporter ('xslt-txt' , 'txt' )
751
790
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
+
752
860
# Reporter class names are defined twice to speed up mypy startup, as this
753
861
# module is slow to import. Ensure that the two definitions match.
754
862
assert set (reporter_classes ) == set (REPORTER_NAMES )
0 commit comments