Skip to content

Commit 9a2d7b6

Browse files
committed
Implement XML-based reports
1 parent bc10d58 commit 9a2d7b6

File tree

12 files changed

+464
-15
lines changed

12 files changed

+464
-15
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ __pycache__
66
/build
77
/env
88
docs/build/
9+
/out/

lxml/__init__.pyi

Whitespace-only changes.

lxml/etree.pyi

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Hand-written stub for lxml.etree as used by mypy.report.
2+
# This is *far* from complete, and the stubgen-generated ones crash mypy.
3+
# Any use of `Any` below means I couldn't figure out the type.
4+
5+
import typing
6+
from typing import Any, Dict, List, Tuple, Union
7+
8+
9+
class _Element:
10+
def addprevious(self, element: '_Element') -> None:
11+
pass
12+
13+
class _ElementTree:
14+
def write(self,
15+
file: Union[str, typing.IO],
16+
encoding: str = None,
17+
method: str = "xml",
18+
pretty_print: bool = False,
19+
xml_declaration: Any = None,
20+
with_tail: Any = True,
21+
standalone: bool = None,
22+
compression: int = 0,
23+
exclusive: bool = False,
24+
with_comments: bool = True,
25+
inclusive_ns_prefixes: List[str] = None) -> None:
26+
pass
27+
28+
class _XSLTResultTree:
29+
pass
30+
31+
class _XSLTQuotedStringParam:
32+
pass
33+
34+
class XMLParser:
35+
pass
36+
37+
class XMLSchema:
38+
def __init__(self,
39+
etree: Union[_Element, _ElementTree] = None,
40+
file: Union[str, typing.IO] = None) -> None:
41+
pass
42+
43+
def assertValid(self,
44+
etree: Union[_Element, _ElementTree]) -> None:
45+
pass
46+
47+
class XSLTAccessControl:
48+
pass
49+
50+
class XSLT:
51+
def __init__(self,
52+
xslt_input: Union[_Element, _ElementTree],
53+
extensions: Dict[Tuple[str, str], Any] = None,
54+
regexp: bool = True,
55+
access_control: XSLTAccessControl = None) -> None:
56+
pass
57+
58+
def __call__(self,
59+
_input: Union[_Element, _ElementTree],
60+
profile_run: bool = False,
61+
**kwargs: Union[str, _XSLTQuotedStringParam]) -> _XSLTResultTree:
62+
pass
63+
64+
@staticmethod
65+
def strparam(s: str) -> _XSLTQuotedStringParam:
66+
pass
67+
68+
def Element(_tag: str,
69+
attrib: Dict[str, str] = None,
70+
nsmap: Dict[str, str] = None,
71+
**extra: str) -> _Element:
72+
pass
73+
74+
def SubElement(_parent: _Element, _tag: str,
75+
attrib: Dict[str, str] = None,
76+
nsmap: Dict[str, str] = None,
77+
**extra: str) -> _Element:
78+
pass
79+
80+
def ElementTree(element: _Element = None,
81+
file: Union[str, typing.IO] = None,
82+
parser: XMLParser = None) -> _ElementTree:
83+
pass
84+
85+
def ProcessingInstruction(target: str, text: str = None) -> _Element:
86+
pass
87+
88+
def parse(source: Union[str, typing.IO],
89+
parser: XMLParser = None,
90+
base_url: str = None) -> _ElementTree:
91+
pass

mypy/build.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,11 @@ def build(program_path: str,
145145
if alt_lib_path:
146146
lib_path.insert(0, alt_lib_path)
147147

148-
reports = Reports(data_dir, report_dirs)
148+
program_path = program_path or lookup_program(module, lib_path)
149+
if program_text is None:
150+
program_text = read_program(program_path)
151+
152+
reports = Reports(program_path, data_dir, report_dirs)
149153

150154
# Construct a build manager object that performs all the stages of the
151155
# build in the correct order.
@@ -157,10 +161,6 @@ def build(program_path: str,
157161
custom_typing_module=custom_typing_module,
158162
reports=reports)
159163

160-
program_path = program_path or lookup_program(module, lib_path)
161-
if program_text is None:
162-
program_text = read_program(program_path)
163-
164164
# Construct information that describes the initial file. __main__ is the
165165
# implicit module id and the import context is empty initially ([]).
166166
info = StateInfo(program_path, module, [], manager)

mypy/report.py

Lines changed: 187 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Classes for producing HTML reports about imprecision."""
22

33
from abc import ABCMeta, abstractmethod
4+
import cgi
5+
import os
6+
import shutil
47

5-
from typing import Callable, Dict, List
8+
from typing import Callable, Dict, List, cast
69

710
from mypy.types import Type
811
from mypy.nodes import MypyFile, Node
@@ -13,17 +16,25 @@
1316

1417

1518
class Reports:
16-
def __init__(self, data_dir: str, report_dirs: Dict[str, str]) -> None:
19+
def __init__(self, main_file: str, data_dir: str, report_dirs: Dict[str, str]) -> None:
20+
self.main_file = main_file
1721
self.data_dir = data_dir
1822
self.reporters = [] # type: List[AbstractReporter]
23+
self.named_reporters = {} # type: Dict[str, AbstractReporter]
1924

2025
for report_type, report_dir in sorted(report_dirs.items()):
2126
self.add_report(report_type, report_dir)
2227

2328
def add_report(self, report_type: str, report_dir: str) -> 'AbstractReporter':
29+
try:
30+
return self.named_reporters[report_type]
31+
except KeyError:
32+
pass
2433
reporter_cls = reporter_classes[report_type]
2534
reporter = reporter_cls(self, report_dir)
2635
self.reporters.append(reporter)
36+
self.named_reporters[report_type] = reporter
37+
return reporter
2738

2839
def file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None:
2940
for reporter in self.reporters:
@@ -60,4 +71,177 @@ def on_finish(self) -> None:
6071
stats.generate_html_index(self.output_dir)
6172
reporter_classes['old-html'] = OldHtmlReporter
6273

63-
reporter_classes['html'] = reporter_classes['old-html']
74+
class FileInfo:
75+
def __init__(self, name: str, module: str) -> None:
76+
self.name = name
77+
self.module = module
78+
self.counts = [0] * len(stats.precision_names)
79+
80+
def total(self) -> int:
81+
return sum(self.counts)
82+
83+
def attrib(self) -> Dict[str, str]:
84+
return {name: str(val) for name, val in zip(stats.precision_names, self.counts)}
85+
86+
class MemoryXmlReporter(AbstractReporter):
87+
"""Internal reporter that generates XML in memory.
88+
89+
This is used by all other XML-based reporters to avoid duplication.
90+
"""
91+
92+
def __init__(self, reports: Reports, output_dir: str) -> None:
93+
import lxml.etree as etree
94+
95+
super().__init__(reports, output_dir)
96+
97+
self.main_file = reports.main_file
98+
self.xslt_html_path = os.path.join(reports.data_dir, 'xml', 'mypy-html.xslt')
99+
self.css_html_path = os.path.join(reports.data_dir, 'xml', 'mypy-html.css')
100+
xsd_path = os.path.join(reports.data_dir, 'xml', 'mypy.xsd')
101+
self.schema = etree.XMLSchema(etree.parse(xsd_path))
102+
self.last_xml = None # type: etree._ElementTree
103+
self.files = [] # type: List[FileInfo]
104+
105+
def on_file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None:
106+
import lxml.etree as etree
107+
108+
self.last_xml = None
109+
path = os.path.relpath(tree.path)
110+
if stats.is_special_module(path):
111+
return
112+
if path.startswith('..'):
113+
return
114+
if 'stubs' in path.split('/'):
115+
return
116+
117+
visitor = stats.StatisticsVisitor(inferred=True, typemap=type_map, all_nodes=True)
118+
tree.accept(visitor)
119+
120+
root = etree.Element('mypy-report-file', name=path, module=tree._fullname)
121+
doc = etree.ElementTree(root)
122+
file_info = FileInfo(path, tree._fullname)
123+
124+
with open(path) as input_file:
125+
for lineno, line_text in enumerate(input_file, 1):
126+
status = visitor.line_map.get(lineno, stats.TYPE_EMPTY)
127+
file_info.counts[status] += 1
128+
etree.SubElement(root, 'line',
129+
number=str(lineno),
130+
precision=stats.precision_names[status],
131+
content=line_text[:-1])
132+
# Assumes a layout similar to what XmlReporter uses.
133+
xslt_path = os.path.relpath('mypy-html.xslt', path)
134+
xml_pi = etree.ProcessingInstruction('xml', 'version="1.0" encoding="utf-8"')
135+
transform_pi = etree.ProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="%s"' % cgi.escape(xslt_path, True))
136+
root.addprevious(xml_pi)
137+
root.addprevious(transform_pi)
138+
self.schema.assertValid(doc)
139+
140+
self.last_xml = doc
141+
self.files.append(file_info)
142+
143+
def on_finish(self) -> None:
144+
import lxml.etree as etree
145+
146+
self.last_xml = None
147+
index_path = os.path.join(self.output_dir, 'index.xml')
148+
output_files = sorted(self.files, key=lambda x: x.module)
149+
150+
root = etree.Element('mypy-report-index', name=self.main_file)
151+
doc = etree.ElementTree(root)
152+
153+
for file_info in output_files:
154+
etree.SubElement(root, 'file',
155+
file_info.attrib(),
156+
total=str(file_info.total()),
157+
name=file_info.name,
158+
module=file_info.module)
159+
xslt_path = os.path.relpath('mypy-html.xslt', '.')
160+
xml_pi = etree.ProcessingInstruction('xml', 'version="1.0" encoding="utf-8"')
161+
transform_pi = etree.ProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="%s"' % cgi.escape(xslt_path, True))
162+
root.addprevious(xml_pi)
163+
root.addprevious(transform_pi)
164+
self.schema.assertValid(doc)
165+
166+
self.last_xml = doc
167+
168+
reporter_classes['memory-xml'] = MemoryXmlReporter
169+
170+
class AbstractXmlReporter(AbstractReporter):
171+
"""Internal abstract class for reporters that work via XML."""
172+
173+
def __init__(self, reports: Reports, output_dir: str) -> None:
174+
super().__init__(reports, output_dir)
175+
176+
memory_reporter = reports.add_report('memory-xml', '<memory>')
177+
# The dependency will be called first.
178+
self.memory_xml = cast(MemoryXmlReporter, memory_reporter)
179+
180+
class XmlReporter(AbstractXmlReporter):
181+
"""Public reporter that exports XML.
182+
183+
The produced XML files contain a reference to the absolute path
184+
of the html transform, so they will be locally viewable in a browser.
185+
186+
However, there is a bug in Chrome and all other WebKit-based browsers
187+
that makes it fail from file:// URLs but work on http:// URLs.
188+
"""
189+
190+
def on_file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None:
191+
last_xml = self.memory_xml.last_xml
192+
if last_xml is None:
193+
return
194+
out_path = os.path.join(self.output_dir, 'xml', tree.path + '.xml')
195+
stats.ensure_dir_exists(os.path.dirname(out_path))
196+
last_xml.write(out_path, encoding='utf-8')
197+
198+
def on_finish(self) -> None:
199+
last_xml = self.memory_xml.last_xml
200+
out_path = os.path.join(self.output_dir, 'index.xml')
201+
out_xslt = os.path.join(self.output_dir, 'mypy-html.xslt')
202+
out_css = os.path.join(self.output_dir, 'mypy-html.css')
203+
last_xml.write(out_path, encoding='utf-8')
204+
shutil.copyfile(self.memory_xml.xslt_html_path, out_xslt)
205+
shutil.copyfile(self.memory_xml.css_html_path, out_css)
206+
print('Generated XML report:', os.path.abspath(out_path))
207+
208+
reporter_classes['xml'] = XmlReporter
209+
210+
class XsltHtmlReporter(AbstractXmlReporter):
211+
"""Public reporter that exports HTML via XSLT.
212+
213+
This is slightly different than running `xsltproc` on the .xml files,
214+
because it passes a parameter to rewrite the links.
215+
"""
216+
217+
def __init__(self, reports: Reports, output_dir: str) -> None:
218+
import lxml.etree as etree
219+
220+
super().__init__(reports, output_dir)
221+
222+
self.xslt_html = etree.XSLT(etree.parse(self.memory_xml.xslt_html_path))
223+
self.param_html = etree.XSLT.strparam('html')
224+
225+
def on_file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None:
226+
last_xml = self.memory_xml.last_xml
227+
if last_xml is None:
228+
return
229+
out_path = os.path.join(self.output_dir, 'html', tree.path + '.html')
230+
stats.ensure_dir_exists(os.path.dirname(out_path))
231+
transformed_html = str(self.xslt_html(last_xml, ext=self.param_html))
232+
with open(out_path, 'wt', encoding='utf-8') as out_file:
233+
out_file.write(transformed_html)
234+
235+
def on_finish(self) -> None:
236+
last_xml = self.memory_xml.last_xml
237+
out_path = os.path.join(self.output_dir, 'index.html')
238+
out_css = os.path.join(self.output_dir, 'mypy-html.css')
239+
transformed_html = str(self.xslt_html(last_xml, ext=self.param_html))
240+
with open(out_path, 'wt', encoding='utf-8') as out_file:
241+
out_file.write(transformed_html)
242+
shutil.copyfile(self.memory_xml.css_html_path, out_css)
243+
print('Generated HTML report (via XSLT):', os.path.abspath(out_path))
244+
245+
reporter_classes['xslt-html'] = XsltHtmlReporter
246+
247+
reporter_classes['html'] = reporter_classes['xslt-html']

mypy/stats.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,17 @@
1818
)
1919

2020

21-
TYPE_PRECISE = 0
22-
TYPE_IMPRECISE = 1
23-
TYPE_ANY = 2
21+
TYPE_EMPTY = 0
22+
TYPE_PRECISE = 1
23+
TYPE_IMPRECISE = 2
24+
TYPE_ANY = 3
25+
26+
precision_names = [
27+
'empty',
28+
'precise',
29+
'imprecise',
30+
'any',
31+
]
2432

2533

2634
class StatisticsVisitor(TraverserVisitor):
@@ -353,7 +361,7 @@ def generate_html_index(output_dir: str) -> None:
353361
append('</body></html>')
354362
with open(path, 'w') as file:
355363
file.writelines(output)
356-
print('Generated HTML report: %s' % os.path.abspath(path))
364+
print('Generated HTML report (old): %s' % os.path.abspath(path))
357365

358366

359367
def ensure_dir_exists(dir: str) -> None:

scripts/mypy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def process_options(args: List[str]) -> Tuple[str, str, Options]:
113113
elif args[0] == '--custom-typing' and args[1:]:
114114
options.custom_typing_module = args[1]
115115
args = args[2:]
116-
elif args[0] in ('--html-report', '--old-html-report') and args[1:]:
116+
elif args[0] in ('--html-report', '--old-html-report', '--xslt-html-report', '--xml-report') and args[1:]:
117117
report_type = args[0][2:-7]
118118
report_dir = args[1]
119119
options.report_dirs[report_type] = report_dir
@@ -160,7 +160,7 @@ Try 'mypy -h' for more information.
160160
Optional arguments:
161161
-h, --help print this help message and exit
162162
--<fmt>-report dir generate a <fmt> report of type precision under dir/
163-
<fmt> may be one of: html, old-html.
163+
<fmt> may be one of: html, old-html, xslt-html, xml.
164164
-m mod type check module
165165
--verbose more verbose messages
166166
--use-python-path search for modules in sys.path of running Python

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ def find_data_files(base, globs):
5656
base = os.path.join('stubs', py_version)
5757
data_files += find_data_files(base, ['*.py', '*.pyi'])
5858

59+
data_files += find_data_files('xml', ['*.xsd', '*.xslt', '*.css'])
60+
5961
classifiers = [
6062
'Development Status :: 2 - Pre-Alpha',
6163
'Environment :: Console',

stubs/3.2/cgi.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
def escape(s: str) -> str: pass
1+
def escape(s: str, quote : bool = False) -> str: pass

0 commit comments

Comments
 (0)