Skip to content

Improve the handling of "iteration dependent" errors and notes in finally clauses. #19270

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

tyralla
Copy link
Collaborator

@tyralla tyralla commented Jun 10, 2025

Fixes #19269

This PR refactors the logic implemented in #19118 (which only targeted repeatedly checked loops) and applies it to repeatedly checked finally clauses.

I moved nearly all relevant code to the class LoopErrorWatcher, which now has the more general name IterationErrorWatcher, to avoid code duplication. However, one duplication is left, which concerns error reporting. It would be nice and easy to move this functionality to IterationErrorWatcher, too, but this would result in import cycles, and I am unsure if working with TYPE_CHECKING and postponed importing is acceptable in such cases (both for Mypy and Mypyc).

After the refactoring, it should not be much effort to apply the logic to other cases where code sections are analysed iteratively. However, the only thing that comes to my mind is the repeated checking of functions with arguments that contain constrained type variables. I will check it. If anyone finds a similar case and the solution is as simple as expected, we could add the fix to this PR, of course.

@tyralla
Copy link
Collaborator Author

tyralla commented Jun 10, 2025

Oh, constrained type variables are indeed a problem, and I just realised this was very recently reported in #19256.

@tyralla tyralla changed the title Improve the handling of "iteration dependend" errors and notes in finally clauses. Improve the handling of "iteration dependent" errors and notes in finally clauses. Jun 10, 2025

This comment has been minimized.

@tyralla
Copy link
Collaborator Author

tyralla commented Jun 10, 2025

According to mypy primer, this fixes at least one real-code problem.

@A5rocks and @JukkaL: Perhaps reviewing this would not require too much effort for you, as #19118 is still fresh on our minds?

@tyralla tyralla requested review from JukkaL and A5rocks June 10, 2025 16:41

This comment has been minimized.

This comment has been minimized.

Copy link
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Looks good overall, but I noticed some issues with how the union types are parsed and constructed. Can you fix the syntax of generated union types in this PR, since the fix seems simple? The error message parsing related issues are best fixed in a separate PR.

mypy/errors.py Outdated

for note_info, types in self.iteration_dependent_errors.revealed_types.items():
sorted_ = sorted(types, key=lambda typ: typ.lower())
revealed = sorted_[0] if len(types) == 1 else f"Union[{', '.join(sorted_)}]"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use | union syntax if targeting 3.10 or later. Can you reuse the functionality already in MessageBuilder.reveal_type to have the type in the correct format?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no direct way to use MessageBuilder.reveal_type/TypeStrVisitor here (for now, but see my general comment below), because they expect types instead of strings, and the types are (currently) not available here. So I just extended the current logic by using Options.use_or_syntax.

(info.code, info.message, info.line, info.column, info.end_line, info.end_column)
)
if info.code == codes.UNREACHABLE:
self.unreachable_lines.update(range(info.line, info.end_line + 1))
iter_errors.unreachable_lines[-1].update(range(info.line, info.end_line + 1))
return True

if info.code == codes.MISC and info.message.startswith("Revealed type is "):
key = info.function_or_member, info.line, info.column, info.end_line, info.end_column
types = info.message.split('"')[1]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic seems unreliable -- consider code like reveal_type('"\''). A better approach would be to keep anything between the first and last double quotes. Since this issue is pre-existing, it's best to fix it in a separate PR.

return True

if info.code == codes.MISC and info.message.startswith("Revealed type is "):
key = info.function_or_member, info.line, info.column, info.end_line, info.end_column
types = info.message.split('"')[1]
if types.startswith("Union["):
self.revealed_types[key].update(types[6:-1].split(", "))
iter_errors.revealed_types[key].update(types[6:-1].split(", "))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's another issue, but since it's pre-existing, it's better to fix in a separate PR: I don't think that the split operation here works reliably -- consider a type like Union[dict[int, str], dict[str, int]], but there are other cases as well, such as nested unions, and unions using the new-style syntax (when targeting 3.10 or later). A correct way to do this would have to be able to skip string literals and keep track of the nesting of parentheses and square brackets at least. It may be easier to keep the original type object.

@tyralla
Copy link
Collaborator Author

tyralla commented Jun 20, 2025

Thank you for the review, @JukkaL! It's great that you've found the existing limitations of my current "revealed type recombination" approach. Besides the simple fix requested for this PR, I played a little and tend to think that storing the original type objects in IterationErrorWatcher.revealed_types might, in fact, be preferable. My initial idea is to define a method like Errors.get_current_iterationerrorwatcher, which could then be used by MessageBuilder.reveal_type to store the original type objects in IterationErrorWatcher.revealed_types instead of constructing a note that is filtered out anyway. Such an approach should be much cleaner and more efficient, I suppose.

I could work on it soon, and it would be good to have this fixed in 1.17 as well. If you think this is not possible, I could also temporarily disable the "revealed type recombination" approach, as the (in my opinion, more relevant) new unreachable functionality does not depend on it.

@tyralla tyralla requested a review from JukkaL June 20, 2025 07:02
Copy link
Contributor

Diff from mypy_primer, showing the effect of this PR on open source code:

xarray (https://github.com/pydata/xarray)
+ xarray/backends/api.py:1943: error: Unused "type: ignore" comment  [unused-ignore]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Wrong unreachable note in finally section
2 participants