ruff icon indicating copy to clipboard operation
ruff copied to clipboard

F401 (unused import) false positive

Open jad-haddad opened this issue 2 years ago • 7 comments

Hello, I have an import that is used in typing and is getting removed by ruff (flake8 detects it as unused as well) here is the example:

from typing import Optional
import datetime # <--- gets removed by ruff
import pydantic

class SomeClass(pydantic.BaseModel):
    datetime: Optional[datetime.datetime]

for now, i'm adding the comment # noqa: F401 to keep the import.

ruff version 0.0.195

Thank you for this project 🤩

jad-haddad avatar Dec 27 '22 11:12 jad-haddad

Thank you! Will check this out.

charliermarsh avatar Dec 27 '22 12:12 charliermarsh

Is this the exact snippet you're using? When I run Ruff on:

from typing import Optional
import datetime
import pydantic

class SomeClass(pydantic.BaseModel):
    datetime: Optional[datetime.datetime]

It doesn't report any errors 🤔 Neither does flake8.

charliermarsh avatar Dec 27 '22 16:12 charliermarsh

(I'm guessing that whatever the problem is, it may have to do with the use of datetime as the class property, which collides with the import name datetime.)

charliermarsh avatar Dec 27 '22 16:12 charliermarsh

well, there are other classes definitions in the module, but none of them have the property datetime nor the type datetime the full list of imports is

from __future__ import annotations
import datetime  # noqa: F401 (false positive)
from enum import Enum
from typing import List, Optional

import pydantic

could it be that the __future__ annotations module is interfering with ruff/flake8 ? I'm using python 3.8 and I have set in ruff.toml target-version = "py38"

jad-haddad avatar Dec 27 '22 17:12 jad-haddad

Oh, yeah, it could be __future__.

charliermarsh avatar Dec 27 '22 17:12 charliermarsh

Here's an minimal reproducible example:

from __future__ import annotations

import datetime
from typing import Optional

class SomeClass:
    datetime: Optional[datetime.datetime]

charliermarsh avatar Dec 27 '22 17:12 charliermarsh

A little challenging to fix, won't happen immediately... My suggestion would be to use a different name for the class attribute, since these kinds of collisions can cause a lot of confusing behavior. For example, Mypy and Pyright handle this case differently, and I don't know which is correct:

from __future__ import annotations

import datetime
from typing import Optional

class SomeClass:
    datetime: Optional[datetime.datetime]

    def __init__(self, datetime: datetime.datetime):
        self.datetime = datetime

SomeClass(datetime.datetime(...))

Mypy flags an error on line 9 (error: Name "datetime.datetime" is not defined), because datetime there is bound to the class property, not the import. But Pyright does not.

charliermarsh avatar Dec 27 '22 17:12 charliermarsh

Is this more or less an extended version of the "A003 problem" where A003 is triggered in the following code because id apparently shadows a Python built-in?

class SomeClass:
    id: int

Basically, datetime (or in my case uuid) is shadowing the import, so that the import becomes unusued. I wouldn't normally expect the second datetime in the field declaration datetime: Optional[datetime.datetime] to refer to the first one, would be a major gotcha for me, does this shadowing really happen?

rassie avatar Jan 01 '23 13:01 rassie

An annotation like id: int or datetime: Optional[datetime.datetime] doesn’t bind anything at runtime, so it shouldn’t be considered as shadowing the outer binding.

An annotated assignment like id: int = 0 or datetime: Optional[datetime.datetime] = datetime.datetime.now(), however, does shadow from the perspective of some references:

class SomeClass:
    #         ↓ shadowed except under __future__.annotations
    datetime: datetime.datetime = (
        # ↓ not shadowed
        datetime.datetime.now()
    )
    #      ↓ shadowed except under __future__.annotations
    other: datetime.datetime = (
        # ↓ always shadowed
        datetime
    )

(The behavior for annotations can be observed at runtime with typing.get_type_hints(SomeClass).)

Mypy’s understanding of annotation shadowing seems to be quite busted. It accepts nonsense like

x = "oops"

class SomeClass:
    x: int
    print(x + 1)

which does str + int at runtime.

andersk avatar Jan 03 '23 04:01 andersk

Came across this today - if it helps here is where the from __future__ import annotations issue was raised in pyflakes

  • https://github.com/PyCQA/pyflakes/issues/648
  • https://github.com/PyCQA/pyflakes/issues/713
  • https://github.com/PyCQA/pyflakes/issues/773

jack-mcivor avatar May 04 '23 13:05 jack-mcivor

I've spent a while looking into this. What we might need is this: https://github.com/microsoft/pyright/commit/27bf8c1f2.

charliermarsh avatar May 18 '23 19:05 charliermarsh

I still have the issue @charliermarsh:

from __future__ import annotations

import datetime  # <--- gets removed by ruff

import pydantic


class SomeClass(pydantic.BaseModel):
    datetime: datetime.datetime

command: ruff --config ruff.toml --show-fixes --fix ./test.py

Fixed 2 errors:
- test.py:
    1 × F401 (unused-import)
    1 × I001 (unsorted-imports)

Ruff 0.0.269 Python 3.11.2

jad-haddad avatar May 24 '23 15:05 jad-haddad

It hasn't been released. It'll be part of 0.0.270.

charliermarsh avatar May 24 '23 15:05 charliermarsh

oh okay my bad, i'll wait for the next release then, thanks :)

jad-haddad avatar May 24 '23 15:05 jad-haddad

Should be out soon, maybe today? We'll see! :)

charliermarsh avatar May 24 '23 15:05 charliermarsh