pyflakes icon indicating copy to clipboard operation
pyflakes copied to clipboard

Handle code guarded by 'if TYPE_CHECKING' correctly

Open PetterS opened this issue 5 years ago • 32 comments

This PR finds a problem that has been observed in real code.

Consider the following code:

from typing import TYPE_CHECKING
...
if TYPE_CHECKING
    from a import A, B
    from b import C

...
def f() -> "B":
    ...

def f()
    # Oops! C is actually used here.
    C()

This PR ignores definitions that are guarded by TYPE_CHECKING in order to find mistakes like this. This constant is always False when mypy is not running anyway.

PetterS avatar Apr 15 '20 17:04 PetterS

there's also if False: and MYPY = False\nif MYPY: and various other conventions for accomplishing the same thing

I don't think skipping the body there is quite right either as it would hide any errors in that block -- the block is not always just imports

your current patch breaks this for instance:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    T = ...

def f() -> 'T':
    pass

asottile avatar Apr 15 '20 17:04 asottile

I'm adding tests demonstrating that: ^ #531

asottile avatar Apr 15 '20 17:04 asottile

There is also this case:

from typing import TYPE_CHECKING
from a import a

if TYPE_CHECKING:
    a()

Which probably should not raise an error? But on second thought maybe it should, since a could be moved inside the check?

PetterS avatar Apr 15 '20 17:04 PetterS

pyflakes currently doesn't do any branch analysis which in some cases requires solving the halting problem

I'd argue that that code should raise an error, as it does during type checking time

asottile avatar Apr 15 '20 17:04 asottile

your current patch breaks this for instance:

This means the solution to this problem will have to be more complicated. Basically imports within the TYPE_CHECKING should be kept around in case they are used in string annotations, but ignored for all other purposes?

PetterS avatar Apr 15 '20 17:04 PetterS

Yes in some capacity, I also don't think this is limited to imports as any code can appear in such a block (functions, classes, etc.)

asottile avatar Apr 15 '20 17:04 asottile

The updated code passes all tests including those added in #531.

PetterS avatar Apr 15 '20 18:04 PetterS

do you plan to handle if False / if MYPY? they're also common ways to approach this problem but I don't think if False is universally used and I don't think if MYPY is reliably detectable as being this particular case -- and the omission of handling those seems odd but I don't quite know the best thing to do there

asottile avatar Apr 15 '20 18:04 asottile

do you plan to handle if False / if MYPY? they're also common ways to approach this problem but I don't think if False is universally used and I don't think if MYPY is reliably detectable as being this particular case -- and the omission of handling those seems odd but I don't quite know the best thing to do there

I wasn't planning on handling those. if False would be feasible I guess but as you say, MYPY could be hard to get right in all cases.

PetterS avatar Apr 15 '20 18:04 PetterS

do you plan to handle if False / if MYPY? they're also common ways to approach this problem but I don't think if False is universally used and I don't think if MYPY is reliably detectable as being this particular case -- and the omission of handling those seems odd but I don't quite know the best thing to do there

I wasn't planning on handling those. if False would be feasible I guess but as you say, MYPY could be hard to get right in all cases.

cool, just checking :)

asottile avatar Apr 15 '20 18:04 asottile

~~Just double-checking: this is a feasible approach then?~~

~~Asking because adding during_type_checking to all Binding subclass constructors and handling all other types of bindings will expand the patch a bit.~~

Never mind, it was easy. Will rebase the PR once we agree on an implementation.

PetterS avatar Apr 15 '20 18:04 PetterS

  • Having a separate stack would mean a lot of work keeping track of both stacks when creating scopes and iterating over the stacks. I don't see how this could be done in a good way. I am not at this point comfortable with any large refactors to this class.
  • Adding a parameter to the constructors of all Binding subclasses would mean a lot of places suddenly needing to know about during_type_checking. Not a good solution either.

I think it is really good to keep this new functionality in a single place. But at this point I almost feel that the current during_type_checking attribute on Binding is the cleanest solution. It follows used in the sense that it is a mutable property, albeit not one meant to be changed after the binding is put into the scope.

Here is another solution that could be acceptable:

  • Add the parameter to all Binding constructors. All different Bindings are then created in a new method:
    def createBinding(self, klass, *args, **kwargs):
      return klass(*args, during_type_checking=self._in_type_checking, **kwargs)
    
    so instead of e.g.
    importation = SubmoduleImportation(alias.name, node)
    
    we would write
    importation = self.createBinding(SubmoduleImportation, alias.name, node)
    

PetterS avatar Apr 16 '20 19:04 PetterS

@asottile

added to each of the types as part of their init signature

The current version of this PR now does this.

PetterS avatar Apr 19 '20 08:04 PetterS

this regresses the following code sample:

from typing import TYPE_CHECKING


if TYPE_CHECKING:
    from typing import TypedDict
else:
    TypedDict = dict


class C(TypedDict):
    x: int

asottile avatar Apr 19 '20 22:04 asottile

Will continue working on this PR when we have CI up and running. We need the following PRs merged:

  • https://github.com/PyCQA/pyflakes/pull/532
  • https://github.com/PyCQA/pyflakes/pull/531

PetterS avatar Apr 26 '20 09:04 PetterS

@PetterS sounds good -- we have a dual sign off and if you feel like you have enough context feel free to approve PRs. I've gone ahead and merged #532 since it's going to block other things anyway. You might also be interested in the the different approach taken by @srittau in #535 -- I hadn't considered an approach like that and it might just work perfectly for your thing as well!

asottile avatar Apr 26 '20 22:04 asottile

Updated this PR with the new tests.

PetterS avatar Apr 27 '20 08:04 PetterS

@asottile merged to the latest master.

PetterS avatar May 13 '20 13:05 PetterS

@asottile Should I merge this to master again? Is this likely to be merged?

PetterS avatar Mar 24 '21 08:03 PetterS

Thanks for the comments! Now I have to remember what I wrote before! 😄 Will try to update the PR soon.

PetterS avatar Mar 26 '21 08:03 PetterS

@asottile made changes based on your review now.

PetterS avatar Mar 26 '21 15:03 PetterS

@asottile ping on this.

PetterS avatar Jun 10 '21 09:06 PetterS

@asottile ping on this.

I already approved, pyflakes (unfortunately) has multi-sign-off

asottile avatar Jun 10 '21 12:06 asottile

@asottile is there anyone specific that should be signing off?

terencehonles avatar Jun 10 '21 18:06 terencehonles

@asottile is there anyone specific that should be signing off?

I've been going with the loosest interpretation of that possible and haven't had any pushback -- that is "anyone" + "a maintainer" being 2

asottile avatar Jun 10 '21 20:06 asottile

LGTM, although I would personally default the for_annotations to False. Especially the test code would be a bit cleaner.

I +1 this suggestion and what I had done in my copy of this same effort #622. I didn't bring it up because it's kinda a nit, but since the comment has been made I'll let @PetterS know there is at least one other person who was biting their tongue before :sweat_smile:

terencehonles avatar Jun 11 '21 22:06 terencehonles

@asottile is there anyone specific that should be signing off?

I've been going with the loosest interpretation of that possible and haven't had any pushback -- that is "anyone" + "a maintainer" being 2

Great, let's merge it then!

PetterS avatar Jun 14 '21 18:06 PetterS

@asottile is there anyone specific that should be signing off?

I've been going with the loosest interpretation of that possible and haven't had any pushback -- that is "anyone" + "a maintainer" being 2

Great, let's merge it then!

wellll now there's two open discussions that should probably be decided / resolved first

asottile avatar Jun 14 '21 18:06 asottile

Any updates on merging this PR?

rggjan avatar Sep 21 '22 13:09 rggjan

Any updates on merging this PR?

just checking:

are you able to see the merge conflict message?

screenshot of merge conflict

and the comment about unresolved discussions?

screenshot of my comment

asottile avatar Sep 21 '22 13:09 asottile