Skip to content
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

Generate one fix per statement for flake8-type-checking rules #4915

Merged
merged 1 commit into from Jun 8, 2023

Conversation

charliermarsh
Copy link
Member

Summary

We have special-case handling for the unused imports diagnostic, whereby we generate one diagnostic per import member, but apply a unified fix on a per-statement basis. This strikes the balance between providing granular feedback in the LSP (i.e., being able to highlight individual members as unused), but allowing Code Actions to fix the entire statement in one pass.

This PR applies the same treatment to the flake8-type-checking rules. So, e.g., if you have from pandas import Series, DataFrame and both Series and DataFrame are only used in typing contexts, we'll generate two diagnostics (one for each member), but those diagnostics will share a fix, and that fix will move both members.

Test Plan

Added some additional snapshots.

checker.stylist,
)?;
Ok(Fix::automatic(edit).isolate(checker.isolation(parent)))
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I cleaned the implementation here up a bit, to use more of our modern conveniences (like binding.trimmed_range).

@@ -61,72 +67,168 @@ impl Violation for RuntimeImportInTypeCheckingBlock {
/// TCH004
pub(crate) fn runtime_import_in_type_checking_block(
Copy link
Member Author

Choose a reason for hiding this comment

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

This, typing_only_runtime_import, and unused_import are structurally very similar. They all follow the same pattern:

  • Iterate over individual imports, and group them by statement of origin.
  • Iterate over the statements, and generate a single fix for all imports (taking into account suppression comments).
  • Generate a diagnostic for each import in the statement, using that single fix.
  • (Generate a diagnostic for each suppressed violation, so that the suppression comments aren't marked as unused.)

A lot of the complexity comes from handling suppression comments, since we need the unified fix to omit those members that are suppressed, and we rarely do that kind of thing in lint rules (we typically leave suppression to the next phase).

Also, while these three files are structurally similar, there are a bunch of nuanced differences between them that make me not want to bother DRYing them up much further. For example, in the typing-only import rule, we have to not only group by statement, but also by import type, since a single import statement can contain imports of multiple types (e.g., import os, pandas), which means we need a fix for each import type.

@charliermarsh charliermarsh force-pushed the charlie/consolidated-typing-errors branch from 562bc03 to e3dcfee Compare June 7, 2023 03:11
@github-actions
Copy link

github-actions bot commented Jun 7, 2023

PR Check Results

Ecosystem

ℹ️ ecosystem check detected changes. (+5, -0, 0 error(s))

disnake (+5, -0)

+ disnake/ext/commands/common_bot_base.py:35:12: TCH004 Move import `importlib.machinery` out of type-checking block. Import is used for more than type hinting.
+ tests/ext/commands/test_core.py:9:35: TCH004 Move import `typing_extensions.assert_type` out of type-checking block. Import is used for more than type hinting.
+ tests/ui/test_action_row.py:13:35: TCH004 Move import `typing_extensions.assert_type` out of type-checking block. Import is used for more than type hinting.
+ tests/ui/test_action_row.py:15:28: TCH004 Move import `disnake.ui.MessageUIComponent` out of type-checking block. Import is used for more than type hinting.
+ tests/ui/test_action_row.py:15:48: TCH004 Move import `disnake.ui.ModalUIComponent` out of type-checking block. Import is used for more than type hinting.

Rules changed: 1
Rule Changes Additions Removals
TCH004 5 5 0

Benchmark

Linux

group                                      main                                   pr
-----                                      ----                                   --
formatter/large/dataset.py                 1.00      5.5±0.02ms     7.4 MB/sec    1.00      5.5±0.02ms     7.4 MB/sec
formatter/numpy/ctypeslib.py               1.02   1092.4±5.86µs    15.2 MB/sec    1.00   1075.0±1.42µs    15.5 MB/sec
formatter/numpy/globals.py                 1.04    124.3±0.40µs    23.7 MB/sec    1.00    119.7±0.19µs    24.6 MB/sec
formatter/pydantic/types.py                1.01      2.4±0.00ms    10.6 MB/sec    1.00      2.4±0.01ms    10.6 MB/sec
linter/all-rules/large/dataset.py          1.01     14.3±0.05ms     2.8 MB/sec    1.00     14.2±0.09ms     2.9 MB/sec
linter/all-rules/numpy/ctypeslib.py        1.00      3.4±0.01ms     4.9 MB/sec    1.00      3.4±0.01ms     4.9 MB/sec
linter/all-rules/numpy/globals.py          1.00    416.2±0.63µs     7.1 MB/sec    1.00    417.9±4.11µs     7.1 MB/sec
linter/all-rules/pydantic/types.py         1.00      5.9±0.03ms     4.3 MB/sec    1.01      6.0±0.06ms     4.3 MB/sec
linter/default-rules/large/dataset.py      1.00      6.8±0.03ms     6.0 MB/sec    1.00      6.8±0.03ms     6.0 MB/sec
linter/default-rules/numpy/ctypeslib.py    1.01   1460.5±1.78µs    11.4 MB/sec    1.00   1451.3±3.94µs    11.5 MB/sec
linter/default-rules/numpy/globals.py      1.01    162.8±0.26µs    18.1 MB/sec    1.00    161.4±0.76µs    18.3 MB/sec
linter/default-rules/pydantic/types.py     1.01      3.0±0.01ms     8.4 MB/sec    1.00      3.0±0.03ms     8.4 MB/sec

Windows

group                                      main                                   pr
-----                                      ----                                   --
formatter/large/dataset.py                 1.00      5.9±0.08ms     6.8 MB/sec    1.00      6.0±0.07ms     6.8 MB/sec
formatter/numpy/ctypeslib.py               1.00  1145.8±32.68µs    14.5 MB/sec    1.00  1140.4±19.83µs    14.6 MB/sec
formatter/numpy/globals.py                 1.00    126.3±3.50µs    23.4 MB/sec    1.01    127.1±6.49µs    23.2 MB/sec
formatter/pydantic/types.py                1.00      2.6±0.06ms     9.8 MB/sec    1.00      2.6±0.05ms     9.8 MB/sec
linter/all-rules/large/dataset.py          1.00     14.8±0.17ms     2.8 MB/sec    1.00     14.8±0.15ms     2.8 MB/sec
linter/all-rules/numpy/ctypeslib.py        1.00      3.7±0.06ms     4.4 MB/sec    1.00      3.8±0.06ms     4.4 MB/sec
linter/all-rules/numpy/globals.py          1.01    439.3±9.18µs     6.7 MB/sec    1.00    435.6±6.53µs     6.8 MB/sec
linter/all-rules/pydantic/types.py         1.00      6.2±0.10ms     4.1 MB/sec    1.01      6.3±0.09ms     4.1 MB/sec
linter/default-rules/large/dataset.py      1.00      7.3±0.07ms     5.6 MB/sec    1.00      7.3±0.08ms     5.6 MB/sec
linter/default-rules/numpy/ctypeslib.py    1.00  1546.3±59.03µs    10.8 MB/sec    1.00  1551.7±25.40µs    10.7 MB/sec
linter/default-rules/numpy/globals.py      1.01    174.2±3.60µs    16.9 MB/sec    1.00    172.8±4.72µs    17.1 MB/sec
linter/default-rules/pydantic/types.py     1.00      3.3±0.04ms     7.7 MB/sec    1.01      3.3±0.05ms     7.6 MB/sec

@charliermarsh charliermarsh force-pushed the charlie/consolidated-typing-errors branch from e3dcfee to 98854d1 Compare June 7, 2023 03:56
qualified_name: &'a str,
/// The first reference to the imported symbol.
reference_id: ReferenceId,
/// The trimmed range of the import.
Copy link
Member

Choose a reason for hiding this comment

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

Is this like List in from typing import List, Sequence?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I'll add a note.

2 3 |
3 4 | if TYPE_CHECKING:
4 |- from typing import List, Sequence, Set
5 |+ from typing import Sequence, Set
5 |+ pass
Copy link
Member

Choose a reason for hiding this comment

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

The is an unnecessary pass state; i'd expected it to be cleaned up by another rule but it seems there is no rule currently that detects extra pass statements? not sure if this is actionable for this PR tough

Copy link
Member Author

Choose a reason for hiding this comment

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

We do have a rule that detects empty type-checking blocks, so this would be picked up from that at least.

@charliermarsh
Copy link
Member Author

I think the ecosystem CI checks must be stale -- I've checked multiple times locally, and I get those same results on main as on this branch. Perhaps the repo was updated between runs?

@charliermarsh charliermarsh force-pushed the charlie/consolidated-typing-errors branch from 98854d1 to 0c84877 Compare June 8, 2023 02:13
@charliermarsh charliermarsh merged commit 4b78141 into main Jun 8, 2023
15 checks passed
@charliermarsh charliermarsh deleted the charlie/consolidated-typing-errors branch June 8, 2023 02:22
@henryiii
Copy link
Contributor

I think maybe this PR did it (?), but the following code, setting up a type alias:

if typing.TYPE_CHECKING:
    import numpy.typing

    FloatArray = numpy.typing.NDArray[numpy.float64]
else:
    FloatArray = numpy.ndarray

Is now being flagged in 0.272, and wasn't in 0.270.

@charliermarsh
Copy link
Member Author

Thanks, I'll take a look.

@charliermarsh
Copy link
Member Author

Are you able to include a snippet to reproduce whatever you're seeing? Or link me to the file?

@henryiii
Copy link
Contributor

henryiii commented Jun 13, 2023

@charliermarsh
Copy link
Member Author

It has to do with how we treat submodule imports. Generally, if you import a and import a.b, then if you use a at runtime, we don't treat a.b usages as "typing-only" unless you enable the "strict" setting -- since import a typically imports a.b anyway.

numpy.typing and a few other modules don't behave this way. I honestly might just special-case them.

@charliermarsh
Copy link
Member Author

I don't quite know the right language to describe it, but here's the same issue in importlib: #3821.

@henryiii
Copy link
Contributor

It's somewhat common to have some typing-only code in a submodule (somewhat often called *.typing). The module above, vector._typeutils, is the same way (though really only intended for internal use).

What strict setting? I usually like strict settings. :)

@charliermarsh
Copy link
Member Author

Hah, I was thinking of https://beta.ruff.rs/docs/settings/#flake8-type-checking-strict, but it doesn't actually fix your issue, let me look at it again.

@charliermarsh
Copy link
Member Author

This does fix the specific issue here:

if typing.TYPE_CHECKING:
    import numpy.typing

    FloatArray = numpy.typing.NDArray[numpy.float64]
else:
    import numpy

    FloatArray = numpy.ndarray

In the end, I think it's actually about the lack of branch analysis. Once we see the import numpy.typing in the TYPE_CHECKING block, we then attribute FloatArray = numpy.ndarray to that import, even though it's in a different branch.

@henryiii
Copy link
Contributor

I applied that method to vector, and it's passing. I'm fine with that for now, especially since that's the only reason numpy is imported in this case, maybe it could be improved eventually.

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.

None yet

3 participants