Skip to content

[pyupgrade] Gated behind a preview of separate diagnostics (UP010)#20020

Closed
IDrokin117 wants to merge 4 commits intoastral-sh:mainfrom
IDrokin117:feat/ruff-19561-preview
Closed

[pyupgrade] Gated behind a preview of separate diagnostics (UP010)#20020
IDrokin117 wants to merge 4 commits intoastral-sh:mainfrom
IDrokin117:feat/ruff-19561-preview

Conversation

@IDrokin117
Copy link
Contributor

@IDrokin117 IDrokin117 commented Aug 21, 2025

Summary

Second part of #19769

Separate diagnostic generation for each unused import and range updating accordingly. Gated behind a preview

Test Plan

Added preview snapshots

@github-actions
Copy link
Contributor

github-actions bot commented Aug 21, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@codspeed-hq
Copy link

codspeed-hq bot commented Aug 21, 2025

CodSpeed Performance Report

Merging #20020 will not alter performance

Comparing IDrokin117:feat/ruff-19561-preview (affcf04) with main (542f080)

Summary

✅ 30 untouched
⏩ 13 skipped1

Footnotes

  1. 13 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@IDrokin117 IDrokin117 marked this pull request as draft August 21, 2025 12:28
@IDrokin117 IDrokin117 force-pushed the feat/ruff-19561-preview branch from d94ab32 to 6b2d9f7 Compare August 21, 2025 13:04
@IDrokin117 IDrokin117 marked this pull request as ready for review August 21, 2025 13:05
@IDrokin117 IDrokin117 force-pushed the feat/ruff-19561-preview branch from 6b2d9f7 to f0419d2 Compare September 8, 2025 17:45
@IDrokin117
Copy link
Contributor Author

@ntBre Could you please review PR?

@ntBre ntBre self-requested a review September 8, 2025 18:01
@ntBre ntBre added the preview Related to preview mode features label Sep 8, 2025
@ntBre
Copy link
Contributor

ntBre commented Sep 8, 2025

Yes, sorry for the delay! We've been preparing for the 0.13 release this past week. I'll be catching up on reviews once that goes out.

@MichaReiser
Copy link
Member

What's the motivation for splitting the diagnostics? This seems unnecessarily verbose to me. Could we instead use the new sub-diagnostics to highlight the individual ranges?

@ntBre
Copy link
Contributor

ntBre commented Sep 19, 2025

What I liked about the split diagnostics in #19769 (review) was mostly just underlining the affected imports instead of the whole import statement, which I think could also be handled by sub-diagnostics.

As one supporting example, we also emit one diagnostic per import for unused-import (F401). Although we could revisit that as well now that we have sub-diagnostics.

I don't feel too strongly either way, though.

playground

@IDrokin117
Copy link
Contributor Author

IDrokin117 commented Sep 23, 2025

@ntBre What should I do here? It seems diagnostics work per import as for F401, doesn't it? Should I use sub-diagnostics instead? What piece of code can I take as a "sub-diagnostic" example?

@ntBre
Copy link
Contributor

ntBre commented Sep 25, 2025

I think we just need to reach agreement on how many diagnostics to emit. If you want to try sub-diagnostics to see how they look, here is a previous comment I used on another PR:

The easiest way is with secondary_annotation on the guard returned by report_diagnostic:

/// Add a secondary annotation with the given message and range.
pub(crate) fn secondary_annotation<'a>(
&mut self,

Here's the one use of it we've added so far:

diagnostic.secondary_annotation(
format_args!("previous definition of `{name}` here"),
shadowed,
);

And an example of the type of diagnostic it produces (the ----- underlining is used for secondary annotations):

F811 Redefinition of unused `bar` from line 6
--> F811_0.py:10:5
|
10 | def bar():
| ^^^ `bar` redefined here
11 | pass
|
::: F811_0.py:6:5
|
5 | @foo
6 | def bar():
| --- previous definition of `bar` here
7 | pass
|

There are some other options if that doesn't look good, but that's the easiest place to start.


That's not technically a sub-diagnostic, but we've only added a nice helper for secondary annotations rather than SubDiagnostics proper. The helper code would be quite similar to the annotation version, but I'm happy to help with that if we go that route.

@IDrokin117 IDrokin117 force-pushed the feat/ruff-19561-preview branch 3 times, most recently from 544c1c5 to 846deed Compare September 30, 2025 19:02
@IDrokin117 IDrokin117 force-pushed the feat/ruff-19561-preview branch from 846deed to 2f8a912 Compare September 30, 2025 19:29
@IDrokin117 IDrokin117 closed this Sep 30, 2025
@IDrokin117 IDrokin117 reopened this Sep 30, 2025
@IDrokin117
Copy link
Contributor Author

@ntBre @MichaReiser I've rewritten the diagnostic using secondary_annotation. Please check it and let me know how it looks now.
I've also handled the edge case when there is only one alias in the import statement, so there is no need to underline that specific alias separately.

Comment on lines +7 to +8
1 | from __future__ import nested_scopes, generators
| ^^^^^^^^^^^^^^^^^^^^^^^-------------^^----------
Copy link
Contributor

Choose a reason for hiding this comment

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

I think if we do go this route, we should narrow the primary diagnostic range to just __future__ (or maybe from __future__ or from __future__ import) to keep the two ranges from overlapping:

Suggested change
1 | from __future__ import nested_scopes, generators
| ^^^^^^^^^^^^^^^^^^^^^^^-------------^^----------
1 | from __future__ import nested_scopes, generators
| ^^^^^^^^^^ ------------- ----------

I think I'd still prefer either one diagnostic per import, or just leaving the diagnostic as it is on stable, over this, though. But I'm curious to hear what Micha thinks. Thank you for exploring this!

Copy link
Member

Choose a reason for hiding this comment

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

I'd have to double-check but narrowing the range changes the places where suppression comments are allowed.

E.g, the following currently works but I suspect won't work if we narrow the range

from __future__ import (
	nested_scopes, # noqa <RULE>
 	generators
)

Copy link
Member

Choose a reason for hiding this comment

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

Hmm yeah, using multiple annotated ranges and highlighting the entire import statement doesn't help improve readability.

I only found this usage in cargo:

warning: methods `unused1` and `unused2` are never used
  --> src/function/maybe_changed_after.rs:41:19
   |
32 | impl VerifyResult {
   | ----------------- methods in this implementation
...
41 |     pub(crate) fn unused1(&self) {}
   |                   ^^^^^^^
42 |
43 |     pub(crate) fn unused2(&self) {}
   |                   ^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

They use the unused1 as the primary range and mark the impl block as a secondary annotation. But doing the same in Ruff requires figuring out how to make our suppression system understand that this diagnostic represents two separate violations (the current assumption is that diagnostics only represent a single error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, would narrowing the noqa range actually be a benefit of separate diagnostics here? Something like this doesn't currently work:

from __future__ import ( 
    nested_scopes,  # noqa: UP010
    generators,
) 

generators

You have to put the noqa here:

from __future__ import ( # noqa: UP010
    nested_scopes,  
    generators,
) 

generators

at least that's the only place I found that works.

But yeah, I think secondary annotations are probably better for highlighting additional context that helps to explain the main diagnostic but likely falls outside the normal snippet context window, like in your Rust example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It appears that the previous implementation, which uses separate diagnostics, functions well for # noqa: UP010 per import object. Should I revert to that approach?

Copy link
Member

Choose a reason for hiding this comment

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

I'm leaning towards leaving it as is. I don't think that any of the tried approaches greatly improve the rendered diagnostics to justify a breaking change. But maybe Brent thinks differently? I'd otherwise close this PR and suggest that we revisit the rendering once we have figured out how to better handle those cases with our diagnostic system. But thank you a lot for trying out all those different approaches. It was very helpful to drive the decision-making and show where there's some need to improve our diagnostic system.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think I agree with Micha. Thank you for working on this, I also agree that it was very helpful to see the different options!

@MichaReiser MichaReiser closed this Oct 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Related to preview mode features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants