From f174dcaca41011c3ce07948ada2af0a0f2f876e4 Mon Sep 17 00:00:00 2001 From: Breno Raisch Date: Sun, 4 May 2025 18:57:54 -0300 Subject: [PATCH 1/3] Work in Progress CallableType usando pretty_callable_or_overload --- mypy/messages.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 2e07d7f63498..f04097d1140a 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -404,22 +404,38 @@ def has_no_attr( self.unsupported_left_operand(op, original_type, context) return codes.OPERATOR elif member == "__neg__": + display_type = ( + self.pretty_callable_or_overload(original_type) + if isinstance(original_type, CallableType) + else format_type(original_type, self.options) +) self.fail( - f"Unsupported operand type for unary - ({format_type(original_type, self.options)})", - context, - code=codes.OPERATOR, - ) + f"Unsupported operand type for unary - ({display_type})", + context, + code=codes.OPERATOR, +) return codes.OPERATOR elif member == "__pos__": + + display_type=( + self.pretty_callable_or_overload(original_type) + if isinstance(original_type, CallableType) + else format_type(original_type, self.options) + ) self.fail( - f"Unsupported operand type for unary + ({format_type(original_type, self.options)})", + f"Unsupported operand type for unary + ({display_type})", context, code=codes.OPERATOR, ) return codes.OPERATOR elif member == "__invert__": + display_type=( + self.pretty_callable_or_overload(original_type) + if isinstance(original_type, CallableType) + else format_type(original_type, self.options) + ) self.fail( - f"Unsupported operand type for ~ ({format_type(original_type, self.options)})", + f"Unsupported operand type for ~ ({display_type})", context, code=codes.OPERATOR, ) From 557d9649c150a1acc29cba8001c15b87b1e8a6b1 Mon Sep 17 00:00:00 2001 From: Breno Raisch Date: Mon, 2 Jun 2025 20:00:36 -0300 Subject: [PATCH 2/3] Add direct HTML reporter to replace XSLT approach (fixes #909) --- mypy/html_report.py | 278 ++++++++++++++++++++++++++++++++++++++++++++ mypy/report.py | 7 ++ 2 files changed, 285 insertions(+) create mode 100644 mypy/html_report.py diff --git a/mypy/html_report.py b/mypy/html_report.py new file mode 100644 index 000000000000..7a0611e9f97f --- /dev/null +++ b/mypy/html_report.py @@ -0,0 +1,278 @@ +"""Classes for producing HTML reports about type checking results.""" + +from __future__ import annotations + +import collections +import os +import shutil +from typing import Any + +from mypy import stats +from mypy.nodes import Expression, MypyFile +from mypy.options import Options +from mypy.report import AbstractReporter, FileInfo, iterate_python_lines, register_reporter, should_skip_path +from mypy.types import Type, TypeOfAny +from mypy.version import __version__ + +# Map of TypeOfAny enum values to descriptive strings +type_of_any_name_map = { + TypeOfAny.unannotated: "Unannotated", + TypeOfAny.explicit: "Explicit", + TypeOfAny.from_unimported_type: "Unimported", + TypeOfAny.from_omitted_generics: "Omitted Generics", + TypeOfAny.from_error: "Error", + TypeOfAny.special_form: "Special Form", + TypeOfAny.implementation_artifact: "Implementation Artifact", +} + + +class MemoryHtmlReporter(AbstractReporter): + """Internal reporter that generates HTML in memory. + + This is used by the HTML reporter to avoid duplication. + """ + + def __init__(self, reports: Any, output_dir: str) -> None: + super().__init__(reports, output_dir) + self.css_html_path = os.path.join(reports.data_dir, "xml", "mypy-html.css") + self.last_html: dict[str, str] = {} # Maps file paths to HTML content + self.index_html: str | None = None + self.files: list[FileInfo] = [] + + def on_file( + self, + tree: MypyFile, + modules: dict[str, MypyFile], + type_map: dict[Expression, Type], + options: Options, + ) -> None: + try: + path = os.path.relpath(tree.path) + except ValueError: + return + + if should_skip_path(path) or os.path.isdir(path): + return # `path` can sometimes be a directory, see #11334 + + visitor = stats.StatisticsVisitor( + inferred=True, + filename=tree.fullname, + modules=modules, + typemap=type_map, + all_nodes=True, + ) + tree.accept(visitor) + + file_info = FileInfo(path, tree._fullname) + + # Generate HTML for this file + html_lines = [ + "", + "", + "", + " ", + " Mypy Report: " + path + "", + " ", + " ", + "", + "", + f"

Mypy Type Check Report for {path}

", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ] + + for lineno, line_text in iterate_python_lines(path): + status = visitor.line_map.get(lineno, stats.TYPE_EMPTY) + file_info.counts[status] += 1 + + precision = stats.precision_names[status] + any_info = self._get_any_info_for_line(visitor, lineno) + + # Escape HTML special characters in the line content + content = line_text.rstrip("\n") + content = content.replace("&", "&").replace("<", "<").replace(">", ">") + + # Add CSS class based on precision + css_class = precision.lower() + + html_lines.append( + f" " + f"" + f"" + f"" + f"" + "" + ) + + html_lines.extend([ + "
LinePrecisionCodeNotes
{lineno}{precision}
{content}
{any_info}
", + "", + "" + ]) + + self.last_html[path] = "\n".join(html_lines) + self.files.append(file_info) + + @staticmethod + def _get_any_info_for_line(visitor: stats.StatisticsVisitor, lineno: int) -> str: + if lineno in visitor.any_line_map: + result = "Any Types on this line: " + counter: collections.Counter[int] = collections.Counter() + for typ in visitor.any_line_map[lineno]: + counter[typ.type_of_any] += 1 + for any_type, occurrences in counter.items(): + result += f"
{type_of_any_name_map[any_type]} (x{occurrences})" + return result + else: + return "" + + def on_finish(self) -> None: + output_files = sorted(self.files, key=lambda x: x.module) + + # Generate index HTML + html_lines = [ + "", + "", + "", + " ", + " Mypy Report Index", + " ", + " ", + "", + "", + "

Mypy Type Check Report

", + "

Generated with mypy " + __version__ + "

", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " " + ] + + for file_info in output_files: + counts = file_info.counts + html_lines.append( + f" " + f"" + f"" + f"" + f"" + f"" + f"" + f"" + f"" + "" + ) + + html_lines.extend([ + "
ModuleFilePreciseImpreciseAnyEmptyUnanalyzedTotal
{file_info.module}{file_info.name}{counts[stats.TYPE_PRECISE]}{counts[stats.TYPE_IMPRECISE]}{counts[stats.TYPE_ANY]}{counts[stats.TYPE_EMPTY]}{counts[stats.TYPE_UNANALYZED]}{file_info.total()}
", + "", + "" + ]) + + self.index_html = "\n".join(html_lines) + + +class HtmlReporter(AbstractReporter): + """Public reporter that exports HTML directly. + + This reporter generates HTML files for each Python module and an index.html file. + """ + + def __init__(self, reports: Any, output_dir: str) -> None: + super().__init__(reports, output_dir) + + memory_reporter = reports.add_report("memory-html", "") + assert isinstance(memory_reporter, MemoryHtmlReporter) + # The dependency will be called first. + self.memory_html = memory_reporter + + def on_file( + self, + tree: MypyFile, + modules: dict[str, MypyFile], + type_map: dict[Expression, Type], + options: Options, + ) -> None: + last_html = self.memory_html.last_html + if not last_html: + return + + path = os.path.relpath(tree.path) + if path.startswith("..") or path not in last_html: + return + + out_path = os.path.join(self.output_dir, "html", path + ".html") + os.makedirs(os.path.dirname(out_path), exist_ok=True) + + with open(out_path, "w", encoding="utf-8") as out_file: + out_file.write(last_html[path]) + + def on_finish(self) -> None: + index_html = self.memory_html.index_html + if index_html is None: + return + + out_path = os.path.join(self.output_dir, "index.html") + out_css = os.path.join(self.output_dir, "mypy-html.css") + + with open(out_path, "w", encoding="utf-8") as out_file: + out_file.write(index_html) + + # Copy CSS file if it exists + if os.path.exists(self.memory_html.css_html_path): + shutil.copyfile(self.memory_html.css_html_path, out_css) + else: + # Create a basic CSS file if the original doesn't exist + with open(out_css, "w", encoding="utf-8") as css_file: + css_file.write(""" + body { font-family: Arial, sans-serif; margin: 20px; } + h1 { color: #333; } + table { border-collapse: collapse; width: 100%; } + th { background-color: #f2f2f2; text-align: left; padding: 8px; } + td { padding: 8px; border-bottom: 1px solid #ddd; } + tr.precise { background-color: #dff0d8; } + tr.imprecise { background-color: #fcf8e3; } + tr.any { background-color: #f2dede; } + tr.empty, tr.unanalyzed { background-color: #f9f9f9; } + pre { margin: 0; white-space: pre-wrap; } + a { color: #337ab7; text-decoration: none; } + a:hover { text-decoration: underline; } + """) + + print("Generated HTML report:", os.path.abspath(out_path)) + + +# Register the reporters +register_reporter("memory-html", MemoryHtmlReporter) +register_reporter("html-direct", HtmlReporter) \ No newline at end of file diff --git a/mypy/report.py b/mypy/report.py index 39cd80ed38bf..06104dc08ee7 100644 --- a/mypy/report.py +++ b/mypy/report.py @@ -474,6 +474,9 @@ def __init__(self, reports: Reports, output_dir: str) -> None: self.schema = etree.XMLSchema(etree.parse(xsd_path)) self.last_xml: Any | None = None self.files: list[FileInfo] = [] + + + # XML doesn't like control characters, but they are sometimes # legal in source code (e.g. comments, string literals). @@ -532,6 +535,10 @@ def on_file( self.last_xml = doc self.files.append(file_info) + + + + @staticmethod def _get_any_info_for_line(visitor: stats.StatisticsVisitor, lineno: int) -> str: if lineno in visitor.any_line_map: From bc2fe277370e7d8e0c7eed33a4e7a335444fa461 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:03:07 +0000 Subject: [PATCH 3/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/html_report.py | 66 ++++++++++++++++++++++----------------------- mypy/messages.py | 26 +++++++++--------- mypy/report.py | 7 ----- 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/mypy/html_report.py b/mypy/html_report.py index 7a0611e9f97f..7fd57046bd84 100644 --- a/mypy/html_report.py +++ b/mypy/html_report.py @@ -10,7 +10,13 @@ from mypy import stats from mypy.nodes import Expression, MypyFile from mypy.options import Options -from mypy.report import AbstractReporter, FileInfo, iterate_python_lines, register_reporter, should_skip_path +from mypy.report import ( + AbstractReporter, + FileInfo, + iterate_python_lines, + register_reporter, + should_skip_path, +) from mypy.types import Type, TypeOfAny from mypy.version import __version__ @@ -64,7 +70,7 @@ def on_file( tree.accept(visitor) file_info = FileInfo(path, tree._fullname) - + # Generate HTML for this file html_lines = [ "", @@ -94,23 +100,23 @@ def on_file( " Precision", " Code", " Notes", - " " + " ", ] for lineno, line_text in iterate_python_lines(path): status = visitor.line_map.get(lineno, stats.TYPE_EMPTY) file_info.counts[status] += 1 - + precision = stats.precision_names[status] any_info = self._get_any_info_for_line(visitor, lineno) - + # Escape HTML special characters in the line content content = line_text.rstrip("\n") content = content.replace("&", "&").replace("<", "<").replace(">", ">") - + # Add CSS class based on precision css_class = precision.lower() - + html_lines.append( f" " f"{lineno}" @@ -119,13 +125,9 @@ def on_file( f"{any_info}" "" ) - - html_lines.extend([ - " ", - "", - "" - ]) - + + html_lines.extend([" ", "", ""]) + self.last_html[path] = "\n".join(html_lines) self.files.append(file_info) @@ -144,7 +146,7 @@ def _get_any_info_for_line(visitor: stats.StatisticsVisitor, lineno: int) -> str def on_finish(self) -> None: output_files = sorted(self.files, key=lambda x: x.module) - + # Generate index HTML html_lines = [ "", @@ -176,7 +178,7 @@ def on_finish(self) -> None: " Empty", " Unanalyzed", " Total", - " " + " ", ] for file_info in output_files: @@ -193,13 +195,9 @@ def on_finish(self) -> None: f"{file_info.total()}" "" ) - - html_lines.extend([ - " ", - "", - "" - ]) - + + html_lines.extend([" ", "", ""]) + self.index_html = "\n".join(html_lines) @@ -227,14 +225,14 @@ def on_file( last_html = self.memory_html.last_html if not last_html: return - + path = os.path.relpath(tree.path) if path.startswith("..") or path not in last_html: return - + out_path = os.path.join(self.output_dir, "html", path + ".html") os.makedirs(os.path.dirname(out_path), exist_ok=True) - + with open(out_path, "w", encoding="utf-8") as out_file: out_file.write(last_html[path]) @@ -242,20 +240,21 @@ def on_finish(self) -> None: index_html = self.memory_html.index_html if index_html is None: return - + out_path = os.path.join(self.output_dir, "index.html") out_css = os.path.join(self.output_dir, "mypy-html.css") - + with open(out_path, "w", encoding="utf-8") as out_file: out_file.write(index_html) - + # Copy CSS file if it exists if os.path.exists(self.memory_html.css_html_path): shutil.copyfile(self.memory_html.css_html_path, out_css) else: # Create a basic CSS file if the original doesn't exist with open(out_css, "w", encoding="utf-8") as css_file: - css_file.write(""" + css_file.write( + """ body { font-family: Arial, sans-serif; margin: 20px; } h1 { color: #333; } table { border-collapse: collapse; width: 100%; } @@ -268,11 +267,12 @@ def on_finish(self) -> None: pre { margin: 0; white-space: pre-wrap; } a { color: #337ab7; text-decoration: none; } a:hover { text-decoration: underline; } - """) - + """ + ) + print("Generated HTML report:", os.path.abspath(out_path)) # Register the reporters register_reporter("memory-html", MemoryHtmlReporter) -register_reporter("html-direct", HtmlReporter) \ No newline at end of file +register_reporter("html-direct", HtmlReporter) diff --git a/mypy/messages.py b/mypy/messages.py index f04097d1140a..18885b49ec29 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -405,39 +405,37 @@ def has_no_attr( return codes.OPERATOR elif member == "__neg__": display_type = ( - self.pretty_callable_or_overload(original_type) - if isinstance(original_type, CallableType) - else format_type(original_type, self.options) -) + self.pretty_callable_or_overload(original_type) + if isinstance(original_type, CallableType) + else format_type(original_type, self.options) + ) self.fail( - f"Unsupported operand type for unary - ({display_type})", - context, - code=codes.OPERATOR, -) + f"Unsupported operand type for unary - ({display_type})", + context, + code=codes.OPERATOR, + ) return codes.OPERATOR elif member == "__pos__": - display_type=( + display_type = ( self.pretty_callable_or_overload(original_type) if isinstance(original_type, CallableType) else format_type(original_type, self.options) ) self.fail( - f"Unsupported operand type for unary + ({display_type})", + f"Unsupported operand type for unary + ({display_type})", context, code=codes.OPERATOR, ) return codes.OPERATOR elif member == "__invert__": - display_type=( + display_type = ( self.pretty_callable_or_overload(original_type) if isinstance(original_type, CallableType) else format_type(original_type, self.options) ) self.fail( - f"Unsupported operand type for ~ ({display_type})", - context, - code=codes.OPERATOR, + f"Unsupported operand type for ~ ({display_type})", context, code=codes.OPERATOR ) return codes.OPERATOR elif member == "__getitem__": diff --git a/mypy/report.py b/mypy/report.py index 06104dc08ee7..39cd80ed38bf 100644 --- a/mypy/report.py +++ b/mypy/report.py @@ -474,9 +474,6 @@ def __init__(self, reports: Reports, output_dir: str) -> None: self.schema = etree.XMLSchema(etree.parse(xsd_path)) self.last_xml: Any | None = None self.files: list[FileInfo] = [] - - - # XML doesn't like control characters, but they are sometimes # legal in source code (e.g. comments, string literals). @@ -535,10 +532,6 @@ def on_file( self.last_xml = doc self.files.append(file_info) - - - - @staticmethod def _get_any_info_for_line(visitor: stats.StatisticsVisitor, lineno: int) -> str: if lineno in visitor.any_line_map: