|
1 | 1 | """Classes for producing HTML reports about imprecision."""
|
2 | 2 |
|
3 | 3 | from abc import ABCMeta, abstractmethod
|
| 4 | +import cgi |
| 5 | +import os |
| 6 | +import shutil |
4 | 7 |
|
5 |
| -from typing import Callable, Dict, List |
| 8 | +from typing import Callable, Dict, List, cast |
6 | 9 |
|
7 | 10 | from mypy.types import Type
|
8 | 11 | from mypy.nodes import MypyFile, Node
|
|
13 | 16 |
|
14 | 17 |
|
15 | 18 | 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 |
17 | 21 | self.data_dir = data_dir
|
18 | 22 | self.reporters = [] # type: List[AbstractReporter]
|
| 23 | + self.named_reporters = {} # type: Dict[str, AbstractReporter] |
19 | 24 |
|
20 | 25 | for report_type, report_dir in sorted(report_dirs.items()):
|
21 | 26 | self.add_report(report_type, report_dir)
|
22 | 27 |
|
23 | 28 | 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 |
24 | 33 | reporter_cls = reporter_classes[report_type]
|
25 | 34 | reporter = reporter_cls(self, report_dir)
|
26 | 35 | self.reporters.append(reporter)
|
| 36 | + self.named_reporters[report_type] = reporter |
| 37 | + return reporter |
27 | 38 |
|
28 | 39 | def file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None:
|
29 | 40 | for reporter in self.reporters:
|
@@ -60,4 +71,212 @@ def on_finish(self) -> None:
|
60 | 71 | stats.generate_html_index(self.output_dir)
|
61 | 72 | reporter_classes['old-html'] = OldHtmlReporter
|
62 | 73 |
|
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 | + path = os.path.relpath(tree.path) |
| 196 | + if path.startswith('..'): |
| 197 | + return |
| 198 | + out_path = os.path.join(self.output_dir, 'xml', path + '.xml') |
| 199 | + stats.ensure_dir_exists(os.path.dirname(out_path)) |
| 200 | + last_xml.write(out_path, encoding='utf-8') |
| 201 | + |
| 202 | + def on_finish(self) -> None: |
| 203 | + last_xml = self.memory_xml.last_xml |
| 204 | + out_path = os.path.join(self.output_dir, 'index.xml') |
| 205 | + out_xslt = os.path.join(self.output_dir, 'mypy-html.xslt') |
| 206 | + out_css = os.path.join(self.output_dir, 'mypy-html.css') |
| 207 | + last_xml.write(out_path, encoding='utf-8') |
| 208 | + shutil.copyfile(self.memory_xml.xslt_html_path, out_xslt) |
| 209 | + shutil.copyfile(self.memory_xml.css_html_path, out_css) |
| 210 | + print('Generated XML report:', os.path.abspath(out_path)) |
| 211 | + |
| 212 | +reporter_classes['xml'] = XmlReporter |
| 213 | + |
| 214 | +class XsltHtmlReporter(AbstractXmlReporter): |
| 215 | + """Public reporter that exports HTML via XSLT. |
| 216 | +
|
| 217 | + This is slightly different than running `xsltproc` on the .xml files, |
| 218 | + because it passes a parameter to rewrite the links. |
| 219 | + """ |
| 220 | + |
| 221 | + def __init__(self, reports: Reports, output_dir: str) -> None: |
| 222 | + import lxml.etree as etree |
| 223 | + |
| 224 | + super().__init__(reports, output_dir) |
| 225 | + |
| 226 | + self.xslt_html = etree.XSLT(etree.parse(self.memory_xml.xslt_html_path)) |
| 227 | + self.param_html = etree.XSLT.strparam('html') |
| 228 | + |
| 229 | + def on_file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None: |
| 230 | + last_xml = self.memory_xml.last_xml |
| 231 | + if last_xml is None: |
| 232 | + return |
| 233 | + path = os.path.relpath(tree.path) |
| 234 | + if path.startswith('..'): |
| 235 | + return |
| 236 | + out_path = os.path.join(self.output_dir, 'html', path + '.html') |
| 237 | + stats.ensure_dir_exists(os.path.dirname(out_path)) |
| 238 | + transformed_html = bytes(self.xslt_html(last_xml, ext=self.param_html)) |
| 239 | + with open(out_path, 'wb') as out_file: |
| 240 | + out_file.write(transformed_html) |
| 241 | + |
| 242 | + def on_finish(self) -> None: |
| 243 | + last_xml = self.memory_xml.last_xml |
| 244 | + out_path = os.path.join(self.output_dir, 'index.html') |
| 245 | + out_css = os.path.join(self.output_dir, 'mypy-html.css') |
| 246 | + transformed_html = bytes(self.xslt_html(last_xml, ext=self.param_html)) |
| 247 | + with open(out_path, 'wb') as out_file: |
| 248 | + out_file.write(transformed_html) |
| 249 | + shutil.copyfile(self.memory_xml.css_html_path, out_css) |
| 250 | + print('Generated HTML report (via XSLT):', os.path.abspath(out_path)) |
| 251 | + |
| 252 | +reporter_classes['xslt-html'] = XsltHtmlReporter |
| 253 | + |
| 254 | +class XsltTxtReporter(AbstractXmlReporter): |
| 255 | + """Public reporter that exports TXT via XSLT. |
| 256 | +
|
| 257 | + Currently this only does the summary, not the individual reports. |
| 258 | + """ |
| 259 | + |
| 260 | + def __init__(self, reports: Reports, output_dir: str) -> None: |
| 261 | + import lxml.etree as etree |
| 262 | + |
| 263 | + super().__init__(reports, output_dir) |
| 264 | + |
| 265 | + self.xslt_txt = etree.XSLT(etree.parse(self.memory_xml.xslt_txt_path)) |
| 266 | + |
| 267 | + def on_file(self, tree: MypyFile, type_map: Dict[Node, Type]) -> None: |
| 268 | + pass |
| 269 | + |
| 270 | + def on_finish(self) -> None: |
| 271 | + last_xml = self.memory_xml.last_xml |
| 272 | + out_path = os.path.join(self.output_dir, 'index.txt') |
| 273 | + stats.ensure_dir_exists(os.path.dirname(out_path)) |
| 274 | + transformed_txt = bytes(self.xslt_txt(last_xml)) |
| 275 | + with open(out_path, 'wb') as out_file: |
| 276 | + out_file.write(transformed_txt) |
| 277 | + print('Generated TXT report (via XSLT):', os.path.abspath(out_path)) |
| 278 | + |
| 279 | +reporter_classes['xslt-txt'] = XsltTxtReporter |
| 280 | + |
| 281 | +reporter_classes['html'] = reporter_classes['xslt-html'] |
| 282 | +reporter_classes['txt'] = reporter_classes['xslt-txt'] |
0 commit comments