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

TCH004: false positive for import that is used only in type hints #7214

Closed
WhyNotHugo opened this issue Sep 7, 2023 · 6 comments · Fixed by #7769
Closed

TCH004: false positive for import that is used only in type hints #7214

WhyNotHugo opened this issue Sep 7, 2023 · 6 comments · Fixed by #7769
Labels
multifile-analysis Requires analysis across multiple files type-inference Requires more advanced type inference.

Comments

@WhyNotHugo
Copy link
Contributor

My test.py:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from sentry_sdk._types import Event
    from sentry_sdk._types import Hint

def before_send(event: Event, hint: Hint) -> Event | None:
    pass

Steps to reproduce:

> ruff --select TCH test.py
test.py:4:35: TCH004 [*] Move import `sentry_sdk._types.Event` out of type-checking block. Import is used for more than type hinting.
test.py:5:35: TCH004 [*] Move import `sentry_sdk._types.Hint` out of type-checking block. Import is used for more than type hinting.
Found 2 errors.
[*] 2 potentially fixable with the --fix option.

In this particular case, sentry_sdk only exports types if TYPE_CHECKING, so I cannot move this out of the type checking block.

@WhyNotHugo
Copy link
Contributor Author

Oh, this is half-wrong. I need to include from __future__ import annotations in this file for the example to do what I mean.

I'm not sure that the auto-fix is correct here; the auto-fix moves the imports out of the if TYPE_CHECKING block, but this is not valid in many cases (including this particular one).

The correct fix in this case is to include from __future__ import annotations. However, I suspect that this fix is invalid in some other situations.

@charliermarsh
Copy link
Member

Yeah, unfortunately Python requires that type annotations on function signatures are available at runtime. (This isn't true of all annotations -- for example, annotated assignments outside of the top-level are not evaluated at runtime IIRC.)

The current fix is limited to moving imports as it attempts to avoid changing the semantics of the code. We've explored supporting automatic quoting for annotations (#6001), which would let us handle this case without adding a from __future__ import annotations import. We could also consider a setting to automatically add from __future__ import annotations in such cases, but that's an even bigger change to user code and would not be safe to do by default.

(If you do want to use __future__ annotations here, I might recommend using this with from __future__ import annotations as a required imports.)

@WhyNotHugo
Copy link
Contributor Author

WhyNotHugo commented Sep 8, 2023 via email

@zanieb
Copy link
Member

zanieb commented Sep 8, 2023

We can't know that the imported types are only available when type checking (without adding multifile analysis) and that seems like a relatively rare case. Perhaps it's worth opening a bug report at Sentry? I'm curious why they made that design decision.

We are already using the "suggested" applicability for this fix so once #4185 lands we will not apply this when --fix is used.

@aqeelat
Copy link

aqeelat commented Sep 20, 2023

I have an example that shows another false positive with TCH004:

pyproject.toml:

[tool.ruff]
extend-select = ["TCH"]
line-length = 120
target-version = "py311"

base.py:

import abc
from typing import Generic
from typing import TypeVar


T = TypeVar("T")


class MyBaseClass(abc.ABC, Generic[T]):
    pass

concrete.py:

from __future__ import annotations

from typing import TYPE_CHECKING

from .base import MyBaseClass

if TYPE_CHECKING:
    from collections.abc import Iterator


class MyConcreteClass(MyBaseClass[list[Iterator]]):
    def __init__(self, items: list[Iterator]):
        self.items = items

Error:

concrete.py:8:33: TCH004 [*] Move import `collections.abc.Iterator` out of type-checking block. Import is used for more than type hinting.
Found 1 error.
[*] 1 potentially fixable with the --fix option.

Tbh, I'm not sure if this is a false positive or is a correct error.
We've been using it as class MyConcreteClass(MyBaseClass[list["Iterator"]]) in production without any issues, but when I removed the quotations, this error appeared.

@charliermarsh
Copy link
Member

@aqeelat - Thanks for the clear write-up. I believe that error is correct, since class MyConcreteClass(MyBaseClass[list[Iterator]]): requires that the type is available at runtime? So moving it into a TYPE_CHECKING block is unsafe.

@zanieb zanieb added type-inference Requires more advanced type inference. multifile-analysis Requires analysis across multiple files labels Oct 3, 2023
zanieb added a commit that referenced this issue Oct 6, 2023
Rebase of #5119 authored by
@evanrittenhouse with additional refinements.

## Changes

- Adds `--unsafe-fixes` / `--no-unsafe-fixes` flags to `ruff check`
- Violations with unsafe fixes are not shown as fixable unless opted-in
- Fix applicability is respected now
    - `Applicability::Never` fixes are no longer applied
    - `Applicability::Sometimes` fixes require opt-in
    - `Applicability::Always` fixes are unchanged
- Hints for availability of `--unsafe-fixes` added to `ruff check`
output

## Examples

Check hints at hidden unsafe fixes
```
❯ ruff check example.py --no-cache --select F601,W292
example.py:1:14: F601 Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

We could add an indicator for which violations have hidden fixes in the
future.

Check treats unsafe fixes as applicable with opt-in
```
❯ ruff check example.py --no-cache --select F601,W292 --unsafe-fixes
example.py:1:14: F601 [*] Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 2 fixable with the --fix option.
```

Also can be enabled in the config file

```
❯ cat ruff.toml
unsafe-fixes = true
```

And opted-out per invocation

```
❯ ruff check example.py --no-cache --select F601,W292 --no-unsafe-fixes
example.py:1:14: F601 Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

Diff does not include unsafe fixes
```
❯ ruff check example.py --no-cache --select F601,W292 --diff
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
 x = {'a': 1, 'a': 1}
-print(('foo'))
+print(('foo'))
\ No newline at end of file

Would fix 1 error.
```

Unless there is opt-in
```
❯ ruff check example.py --no-cache --select F601,W292 --diff --unsafe-fixes
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
-x = {'a': 1}
-print(('foo'))
+x = {'a': 1, 'a': 1}
+print(('foo'))
\ No newline at end of file

Would fix 2 errors.
```

#7790 will improve the diff
messages following this pull request

Similarly, `--fix` and `--fix-only` require the `--unsafe-fixes` flag to
apply unsafe fixes.

## Related

Replaces #5119
Closes #4185
Closes #7214
Closes #4845
Closes #3863
Addresses #6835
Addresses #7019
Needs follow-up #6962
Needs follow-up #4845
Needs follow-up #7436
Needs follow-up #7025
Needs follow-up #6434
Follow-up #7790 
Follow-up #7792

---------

Co-authored-by: Evan Rittenhouse <evanrittenhouse@gmail.com>
konstin pushed a commit that referenced this issue Oct 11, 2023
Rebase of #5119 authored by
@evanrittenhouse with additional refinements.

## Changes

- Adds `--unsafe-fixes` / `--no-unsafe-fixes` flags to `ruff check`
- Violations with unsafe fixes are not shown as fixable unless opted-in
- Fix applicability is respected now
    - `Applicability::Never` fixes are no longer applied
    - `Applicability::Sometimes` fixes require opt-in
    - `Applicability::Always` fixes are unchanged
- Hints for availability of `--unsafe-fixes` added to `ruff check`
output

## Examples

Check hints at hidden unsafe fixes
```
❯ ruff check example.py --no-cache --select F601,W292
example.py:1:14: F601 Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

We could add an indicator for which violations have hidden fixes in the
future.

Check treats unsafe fixes as applicable with opt-in
```
❯ ruff check example.py --no-cache --select F601,W292 --unsafe-fixes
example.py:1:14: F601 [*] Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 2 fixable with the --fix option.
```

Also can be enabled in the config file

```
❯ cat ruff.toml
unsafe-fixes = true
```

And opted-out per invocation

```
❯ ruff check example.py --no-cache --select F601,W292 --no-unsafe-fixes
example.py:1:14: F601 Dictionary key literal `'a'` repeated
example.py:2:15: W292 [*] No newline at end of file
Found 2 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
```

Diff does not include unsafe fixes
```
❯ ruff check example.py --no-cache --select F601,W292 --diff
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
 x = {'a': 1, 'a': 1}
-print(('foo'))
+print(('foo'))
\ No newline at end of file

Would fix 1 error.
```

Unless there is opt-in
```
❯ ruff check example.py --no-cache --select F601,W292 --diff --unsafe-fixes
--- example.py
+++ example.py
@@ -1,2 +1,2 @@
-x = {'a': 1}
-print(('foo'))
+x = {'a': 1, 'a': 1}
+print(('foo'))
\ No newline at end of file

Would fix 2 errors.
```

#7790 will improve the diff
messages following this pull request

Similarly, `--fix` and `--fix-only` require the `--unsafe-fixes` flag to
apply unsafe fixes.

## Related

Replaces #5119
Closes #4185
Closes #7214
Closes #4845
Closes #3863
Addresses #6835
Addresses #7019
Needs follow-up #6962
Needs follow-up #4845
Needs follow-up #7436
Needs follow-up #7025
Needs follow-up #6434
Follow-up #7790 
Follow-up #7792

---------

Co-authored-by: Evan Rittenhouse <evanrittenhouse@gmail.com>
justalemon added a commit to justalemon/Leek that referenced this issue Jul 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
multifile-analysis Requires analysis across multiple files type-inference Requires more advanced type inference.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants