Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork3k
Feature: Clear type info on del for local inferred variables (fixes #10005)#19386
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:master
Are you sure you want to change the base?
Changes fromall commits
f174dca
557d964
1dc7c1d
d9d8365
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -5225,9 +5225,24 @@ def visit_del_stmt(self, s: DelStmt) -> None: | ||
s.expr.accept(self.expr_checker) | ||
for elt in flatten(s.expr): | ||
if isinstance(elt, NameExpr): | ||
# For local variables, completely remove type binding to allow reuse | ||
if ( | ||
isinstance(elt.node, Var) | ||
and elt.node.is_inferred | ||
and not elt.node.is_property | ||
and not elt.node.is_classvar | ||
and elt.node.is_local | ||
): | ||
# Completely remove the variable from type tracking | ||
self.binder.cleanse(elt) | ||
# Also remove from type map if present | ||
if hasattr(self, "type_map") and elt in self.type_map: | ||
del self.type_map[elt] | ||
else: | ||
# For non-local variables, use the existing DeletedType behavior | ||
self.binder.assign_type( | ||
elt, DeletedType(source=elt.name), get_declaration(elt) | ||
) | ||
def visit_decorator(self, e: Decorator) -> None: | ||
for d in e.decorators: | ||
@@ -5306,7 +5321,7 @@ def visit_decorator_inner( | ||
self.fail(message_registry.BAD_CONSTRUCTOR_TYPE, e) | ||
if e.func.original_def and isinstance(sig, FunctionLike): | ||
# Functionsignature processing continues herectiondefinition overrides function definition. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. ?? | ||
self.check_func_def_override(e.func, sig) | ||
def check_for_untyped_decorator( | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,278 @@ | ||
"""Classes for producing HTML reports about type checking results.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Does this have anything to do with | ||
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 = [ | ||
"<!DOCTYPE html>", | ||
"<html>", | ||
"<head>", | ||
" <meta charset='utf-8'>", | ||
" <title>Mypy Report: " + path + "</title>", | ||
" <link rel='stylesheet' href='../mypy-html.css'>", | ||
" <style>", | ||
" 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; }", | ||
" </style>", | ||
"</head>", | ||
"<body>", | ||
f" <h1>Mypy Type Check Report for {path}</h1>", | ||
" <table>", | ||
" <tr>", | ||
" <th>Line</th>", | ||
" <th>Precision</th>", | ||
" <th>Code</th>", | ||
" <th>Notes</th>", | ||
" </tr>", | ||
] | ||
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" <tr class='{css_class}'>" | ||
f"<td>{lineno}</td>" | ||
f"<td>{precision}</td>" | ||
f"<td><pre>{content}</pre></td>" | ||
f"<td>{any_info}</td>" | ||
"</tr>" | ||
) | ||
html_lines.extend([" </table>", "</body>", "</html>"]) | ||
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"<br>{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 = [ | ||
"<!DOCTYPE html>", | ||
"<html>", | ||
"<head>", | ||
" <meta charset='utf-8'>", | ||
" <title>Mypy Report Index</title>", | ||
" <link rel='stylesheet' href='mypy-html.css'>", | ||
" <style>", | ||
" 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; }", | ||
" a { color: #337ab7; text-decoration: none; }", | ||
" a:hover { text-decoration: underline; }", | ||
" </style>", | ||
"</head>", | ||
"<body>", | ||
" <h1>Mypy Type Check Report</h1>", | ||
" <p>Generated with mypy " + __version__ + "</p>", | ||
" <table>", | ||
" <tr>", | ||
" <th>Module</th>", | ||
" <th>File</th>", | ||
" <th>Precise</th>", | ||
" <th>Imprecise</th>", | ||
" <th>Any</th>", | ||
" <th>Empty</th>", | ||
" <th>Unanalyzed</th>", | ||
" <th>Total</th>", | ||
" </tr>", | ||
] | ||
for file_info in output_files: | ||
counts = file_info.counts | ||
html_lines.append( | ||
f" <tr>" | ||
f"<td>{file_info.module}</td>" | ||
f"<td><a href='html/{file_info.name}.html'>{file_info.name}</a></td>" | ||
f"<td>{counts[stats.TYPE_PRECISE]}</td>" | ||
f"<td>{counts[stats.TYPE_IMPRECISE]}</td>" | ||
f"<td>{counts[stats.TYPE_ANY]}</td>" | ||
f"<td>{counts[stats.TYPE_EMPTY]}</td>" | ||
f"<td>{counts[stats.TYPE_UNANALYZED]}</td>" | ||
f"<td>{file_info.total()}</td>" | ||
"</tr>" | ||
) | ||
html_lines.extend([" </table>", "</body>", "</html>"]) | ||
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", "<memory>") | ||
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -404,24 +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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Is it related to | ||
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, | ||
) | ||
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 + ({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 ~ ({display_type})", context, code=codes.OPERATOR | ||
) | ||
return codes.OPERATOR | ||
elif member == "__getitem__": | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
[case testDelVariableReuse] | ||
def test_del_reuse() -> None: | ||
x = 1 | ||
reveal_type(x) # N: Revealed type is "builtins.int" | ||
del x | ||
x = "hello" | ||
reveal_type(x) # N: Revealed type is "builtins.str" | ||
[case testDelVariableReuseConditional] | ||
def test_del_conditional() -> None: | ||
x = 1 | ||
if True: | ||
del x | ||
x = "hello" | ||
reveal_type(x) # N: Revealed type is "builtins.str" | ||
[case testDelVariableNotLocal] | ||
x = 1 | ||
def test_del_global() -> None: | ||
global x | ||
del x | ||
x = "hello" # E: Incompatible types in assignment (expression has type "str", variable has type "int") | ||
[case testDelIndexExpr] | ||
def test_del_index() -> None: | ||
d = {"key": 1} | ||
del d["key"] | ||
d["key"] = "hello" # OK - this should work normally | ||
[case testDelAttribute] | ||
class C: | ||
attr: int = 1 | ||
def test_del_attr() -> None: | ||
c = C() | ||
del c.attr | ||
c.attr = "hello" # E: Incompatible types in assignment (expression has type "str", target has type "int") |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.