Skip to content

Commit fcc103f

Browse files
committed
Implement XML-based reports
1 parent 8bb09b3 commit fcc103f

16 files changed

+635
-18
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/

lib-typing/3.2/typing.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
# Structural checks, a.k.a. protocols.
5252
'Reversible',
5353
'SupportsAbs',
54+
'SupportsBytes',
55+
'SupportsComplex',
5456
'SupportsFloat',
5557
'SupportsInt',
5658
'SupportsRound',

lxml/__init__.pyi

Whitespace-only changes.

lxml/etree.pyi

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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+
from typing import SupportsBytes
8+
9+
10+
# We do *not* want `typing.AnyStr` because it is a `TypeVar`, which is an
11+
# unnecessary constraint. It seems reasonable to constrain each
12+
# List/Dict argument to use one type consistently, though, and it is
13+
# necessary in order to keep these brief.
14+
AnyStr = Union[str, bytes]
15+
ListAnyStr = Union[List[str], List[bytes]]
16+
DictAnyStr = Union[Dict[str, str], Dict[bytes, bytes]]
17+
Dict_Tuple2AnyStr_Any = Union[Dict[Tuple[str, str], Any], Tuple[bytes, bytes], Any]
18+
19+
20+
class _Element:
21+
def addprevious(self, element: '_Element') -> None:
22+
pass
23+
24+
class _ElementTree:
25+
def write(self,
26+
file: Union[AnyStr, typing.IO],
27+
encoding: AnyStr = None,
28+
method: AnyStr = "xml",
29+
pretty_print: bool = False,
30+
xml_declaration: Any = None,
31+
with_tail: Any = True,
32+
standalone: bool = None,
33+
compression: int = 0,
34+
exclusive: bool = False,
35+
with_comments: bool = True,
36+
inclusive_ns_prefixes: ListAnyStr = None) -> None:
37+
pass
38+
39+
class _XSLTResultTree(SupportsBytes):
40+
pass
41+
42+
class _XSLTQuotedStringParam:
43+
pass
44+
45+
class XMLParser:
46+
pass
47+
48+
class XMLSchema:
49+
def __init__(self,
50+
etree: Union[_Element, _ElementTree] = None,
51+
file: Union[AnyStr, typing.IO] = None) -> None:
52+
pass
53+
54+
def assertValid(self,
55+
etree: Union[_Element, _ElementTree]) -> None:
56+
pass
57+
58+
class XSLTAccessControl:
59+
pass
60+
61+
class XSLT:
62+
def __init__(self,
63+
xslt_input: Union[_Element, _ElementTree],
64+
extensions: Dict_Tuple2AnyStr_Any = None,
65+
regexp: bool = True,
66+
access_control: XSLTAccessControl = None) -> None:
67+
pass
68+
69+
def __call__(self,
70+
_input: Union[_Element, _ElementTree],
71+
profile_run: bool = False,
72+
**kwargs: Union[AnyStr, _XSLTQuotedStringParam]) -> _XSLTResultTree:
73+
pass
74+
75+
@staticmethod
76+
def strparam(s: AnyStr) -> _XSLTQuotedStringParam:
77+
pass
78+
79+
def Element(_tag: AnyStr,
80+
attrib: DictAnyStr = None,
81+
nsmap: DictAnyStr = None,
82+
**extra: AnyStr) -> _Element:
83+
pass
84+
85+
def SubElement(_parent: _Element, _tag: AnyStr,
86+
attrib: DictAnyStr = None,
87+
nsmap: DictAnyStr = None,
88+
**extra: AnyStr) -> _Element:
89+
pass
90+
91+
def ElementTree(element: _Element = None,
92+
file: Union[AnyStr, typing.IO] = None,
93+
parser: XMLParser = None) -> _ElementTree:
94+
pass
95+
96+
def ProcessingInstruction(target: AnyStr, text: AnyStr = None) -> _Element:
97+
pass
98+
99+
def parse(source: Union[AnyStr, typing.IO],
100+
parser: XMLParser = None,
101+
base_url: AnyStr = None) -> _ElementTree:
102+
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: 216 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,206 @@ 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.xslt_txt_path = os.path.join(reports.data_dir, 'xml', 'mypy-txt.xslt')
100+
self.css_html_path = os.path.join(reports.data_dir, 'xml', 'mypy-html.css')
101+
xsd_path = os.path.join(reports.data_dir, 'xml', 'mypy.xsd')
102+
self.schema = etree.XMLSchema(etree.parse(xsd_path))
103+
self.last_xml = None # type: etree._ElementTree
104+
self.files = [] # type: List[FileInfo]
105+
106+
def on_file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None:
107+
import lxml.etree as etree
108+
109+
self.last_xml = None
110+
path = os.path.relpath(tree.path)
111+
if stats.is_special_module(path):
112+
return
113+
if path.startswith('..'):
114+
return
115+
if 'stubs' in path.split('/'):
116+
return
117+
118+
visitor = stats.StatisticsVisitor(inferred=True, typemap=type_map, all_nodes=True)
119+
tree.accept(visitor)
120+
121+
root = etree.Element('mypy-report-file', name=path, module=tree._fullname)
122+
doc = etree.ElementTree(root)
123+
file_info = FileInfo(path, tree._fullname)
124+
125+
with open(path) as input_file:
126+
for lineno, line_text in enumerate(input_file, 1):
127+
status = visitor.line_map.get(lineno, stats.TYPE_EMPTY)
128+
file_info.counts[status] += 1
129+
etree.SubElement(root, 'line',
130+
number=str(lineno),
131+
precision=stats.precision_names[status],
132+
content=line_text[:-1])
133+
# Assumes a layout similar to what XmlReporter uses.
134+
xslt_path = os.path.relpath('mypy-html.xslt', path)
135+
xml_pi = etree.ProcessingInstruction('xml', 'version="1.0" encoding="utf-8"')
136+
transform_pi = etree.ProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="%s"' % cgi.escape(xslt_path, True))
137+
root.addprevious(xml_pi)
138+
root.addprevious(transform_pi)
139+
self.schema.assertValid(doc)
140+
141+
self.last_xml = doc
142+
self.files.append(file_info)
143+
144+
def on_finish(self) -> None:
145+
import lxml.etree as etree
146+
147+
self.last_xml = None
148+
index_path = os.path.join(self.output_dir, 'index.xml')
149+
output_files = sorted(self.files, key=lambda x: x.module)
150+
151+
root = etree.Element('mypy-report-index', name=self.main_file)
152+
doc = etree.ElementTree(root)
153+
154+
for file_info in output_files:
155+
etree.SubElement(root, 'file',
156+
file_info.attrib(),
157+
total=str(file_info.total()),
158+
name=file_info.name,
159+
module=file_info.module)
160+
xslt_path = os.path.relpath('mypy-html.xslt', '.')
161+
xml_pi = etree.ProcessingInstruction('xml', 'version="1.0" encoding="utf-8"')
162+
transform_pi = etree.ProcessingInstruction('xml-stylesheet', 'type="text/xsl" href="%s"' % cgi.escape(xslt_path, True))
163+
root.addprevious(xml_pi)
164+
root.addprevious(transform_pi)
165+
self.schema.assertValid(doc)
166+
167+
self.last_xml = doc
168+
169+
reporter_classes['memory-xml'] = MemoryXmlReporter
170+
171+
class AbstractXmlReporter(AbstractReporter):
172+
"""Internal abstract class for reporters that work via XML."""
173+
174+
def __init__(self, reports: Reports, output_dir: str) -> None:
175+
super().__init__(reports, output_dir)
176+
177+
memory_reporter = reports.add_report('memory-xml', '<memory>')
178+
# The dependency will be called first.
179+
self.memory_xml = cast(MemoryXmlReporter, memory_reporter)
180+
181+
class XmlReporter(AbstractXmlReporter):
182+
"""Public reporter that exports XML.
183+
184+
The produced XML files contain a reference to the absolute path
185+
of the html transform, so they will be locally viewable in a browser.
186+
187+
However, there is a bug in Chrome and all other WebKit-based browsers
188+
that makes it fail from file:// URLs but work on http:// URLs.
189+
"""
190+
191+
def on_file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None:
192+
last_xml = self.memory_xml.last_xml
193+
if last_xml is None:
194+
return
195+
out_path = os.path.join(self.output_dir, 'xml', tree.path + '.xml')
196+
stats.ensure_dir_exists(os.path.dirname(out_path))
197+
last_xml.write(out_path, encoding='utf-8')
198+
199+
def on_finish(self) -> None:
200+
last_xml = self.memory_xml.last_xml
201+
out_path = os.path.join(self.output_dir, 'index.xml')
202+
out_xslt = os.path.join(self.output_dir, 'mypy-html.xslt')
203+
out_css = os.path.join(self.output_dir, 'mypy-html.css')
204+
last_xml.write(out_path, encoding='utf-8')
205+
shutil.copyfile(self.memory_xml.xslt_html_path, out_xslt)
206+
shutil.copyfile(self.memory_xml.css_html_path, out_css)
207+
print('Generated XML report:', os.path.abspath(out_path))
208+
209+
reporter_classes['xml'] = XmlReporter
210+
211+
class XsltHtmlReporter(AbstractXmlReporter):
212+
"""Public reporter that exports HTML via XSLT.
213+
214+
This is slightly different than running `xsltproc` on the .xml files,
215+
because it passes a parameter to rewrite the links.
216+
"""
217+
218+
def __init__(self, reports: Reports, output_dir: str) -> None:
219+
import lxml.etree as etree
220+
221+
super().__init__(reports, output_dir)
222+
223+
self.xslt_html = etree.XSLT(etree.parse(self.memory_xml.xslt_html_path))
224+
self.param_html = etree.XSLT.strparam('html')
225+
226+
def on_file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None:
227+
last_xml = self.memory_xml.last_xml
228+
if last_xml is None:
229+
return
230+
out_path = os.path.join(self.output_dir, 'html', tree.path + '.html')
231+
stats.ensure_dir_exists(os.path.dirname(out_path))
232+
transformed_html = bytes(self.xslt_html(last_xml, ext=self.param_html))
233+
with open(out_path, 'wb') as out_file:
234+
out_file.write(transformed_html)
235+
236+
def on_finish(self) -> None:
237+
last_xml = self.memory_xml.last_xml
238+
out_path = os.path.join(self.output_dir, 'index.html')
239+
out_css = os.path.join(self.output_dir, 'mypy-html.css')
240+
transformed_html = bytes(self.xslt_html(last_xml, ext=self.param_html))
241+
with open(out_path, 'wb') as out_file:
242+
out_file.write(transformed_html)
243+
shutil.copyfile(self.memory_xml.css_html_path, out_css)
244+
print('Generated HTML report (via XSLT):', os.path.abspath(out_path))
245+
246+
reporter_classes['xslt-html'] = XsltHtmlReporter
247+
248+
class XsltTxtReporter(AbstractXmlReporter):
249+
"""Public reporter that exports TXT via XSLT.
250+
251+
Currently this only does the summary, not the individual reports.
252+
"""
253+
254+
def __init__(self, reports: Reports, output_dir: str) -> None:
255+
import lxml.etree as etree
256+
257+
super().__init__(reports, output_dir)
258+
259+
self.xslt_txt = etree.XSLT(etree.parse(self.memory_xml.xslt_txt_path))
260+
261+
def on_file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None:
262+
pass
263+
264+
def on_finish(self) -> None:
265+
last_xml = self.memory_xml.last_xml
266+
out_path = os.path.join(self.output_dir, 'index.txt')
267+
stats.ensure_dir_exists(os.path.dirname(out_path))
268+
transformed_txt = bytes(self.xslt_txt(last_xml))
269+
with open(out_path, 'wb') as out_file:
270+
out_file.write(transformed_txt)
271+
print('Generated TXT report (via XSLT):', os.path.abspath(out_path))
272+
273+
reporter_classes['xslt-txt'] = XsltTxtReporter
274+
275+
reporter_classes['html'] = reporter_classes['xslt-html']
276+
reporter_classes['txt'] = reporter_classes['xslt-txt']

0 commit comments

Comments
 (0)