-
Notifications
You must be signed in to change notification settings - Fork 180
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
Handle code guarded by 'if TYPE_CHECKING' correctly #530
Conversation
there's also 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 |
I'm adding tests demonstrating that: ^ #531 |
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 |
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 |
This means the solution to this problem will have to be more complicated. Basically imports within the |
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.) |
824a12d
to
11a8266
Compare
The updated code passes all tests including those added in #531. |
pyflakes/checker.py
Outdated
scope[n.fullName].used = (self.scope, node) | ||
except KeyError: | ||
pass | ||
if isinstance(n, Importation): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not just imports -- it's any code in there
for instance:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
class C:
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I wasn't too worried about that case, but let's of course fix that!
pyflakes/checker.py
Outdated
@@ -2120,6 +2131,7 @@ def IMPORT(self, node): | |||
else: | |||
name = alias.asname or alias.name | |||
importation = Importation(name, node, alias.name) | |||
importation.during_type_checking = self._in_type_checking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be part of the construction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do! There is quite a hierarchy of classes that will touch but it makes sense I suppose.
This commit finds a problem that has been observed in read 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 commit ignores all code that is guarded by TYPE_CHECKING in order to find mistakes like this. This constant is always False when mypy is not running anyway.
11a8266
to
44aab70
Compare
do you plan to handle |
I wasn't planning on handling those. |
cool, just checking :) |
Never mind, it was easy. Will rebase the PR once we agree on an implementation. |
e2b27b9
to
241262e
Compare
241262e
to
d66aeb8
Compare
pyflakes/checker.py
Outdated
@@ -1072,6 +1074,8 @@ def addBinding(self, node, value): | |||
break | |||
existing = scope.get(value.name) | |||
|
|||
value.during_type_checking = self._in_type_checking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my comment before was because attribute stuffing is a smell (and a type checker such as mypy would error accordingly on this line), this should either be:
- added to each of the types as part of their
__init__
signature - stored separately alongside the type (a tuple?)
and not tacked onto (~essentially immutable by interface) objects after construction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(another alternative)
3. stored in a separate structure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree. But I think we should store it separately somehow since otherwise we will have to change so many places where we create various bindings. Makes sense to only have addBinding
know that this has to be done.
I think this is sound from a type-checking perspective, though? Since value
is documented to be an instance of Binding
we know that this property exists.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@asottile Binding
has another attribute used
that is used in a similar way as during_type_checking
currently is. So it's not exactly immutable but I see what you mean since it's more obvious that used
will change perhaps.
But I think we can store this property somewhere else in the scope
. Shouldn't be too complicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Uploaded an example. Not really sure about it, but at least it is another solution.
This reverts commit a6f0dd2.
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 Here is another solution that could be acceptable:
|
The current version of this PR now does this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool!
pyflakes/test/test_imports.py
Outdated
''', m.UndefinedName) | ||
|
||
@skipIf(version_info < (3,), 'has type annotations') | ||
def test_usesTypingImportsForAnnotations(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fwiw, the new tests are using snake_case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
pyflakes/checker.py
Outdated
self.name = name | ||
self.source = source | ||
self.used = False | ||
assert isinstance(during_type_checking, bool) | ||
self.during_type_checking = during_type_checking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe for_annotations
is a more descriptive name here? (idk, I'm bikeshedding on this, feel free to ignore this suggestion, it just feels a little like the meaning of the name gets lost a bit)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's fine; takes a couple of seconds to change.
@asottile Should I merge this to master again? Is this likely to be merged? |
ce08c41
to
7eb8484
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if not isinstance(body_nodes, list): | ||
body_nodes = [body_nodes] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this code ever run? I think the body is always a list here (?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in IFEXP
body is not a list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, there is a test that will fail if this if statement is removed.
pyflakes/test/test_imports.py
Outdated
@@ -1031,6 +1039,48 @@ def test_futureImportStar(self): | |||
from __future__ import * | |||
''', m.FutureFeatureNotDefined) | |||
|
|||
def test_ignores_typing_imports(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's put these tests into the type_annotations test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
def test_typing_guard_with_elif_branch(self): | ||
# This test will actually not raise an error, even though it by analysis can | ||
# be shown to have an error (Protocol is not defined outside TYPE_CHECKING). | ||
# Pyflakes currently does not to case analysis. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to => do
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the main thing compared to #622 is this doesn't support if not TYPE_CHECKING
, but that's probably not as likely as the other way so it's probably fine to not handle that.
pyflakes/checker.py
Outdated
@@ -1216,6 +1226,10 @@ def handleNodeLoad(self, node): | |||
scope[n.fullName].used = (self.scope, node) | |||
except KeyError: | |||
pass | |||
if n.for_annotations and not self._in_annotation: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this not happen before scope[name].used
is marked? It's not really being used and the variable should be eligible to be released.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do this, the following test will change:
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from a import b
b()
It will also return 'a.b' imported but unused
after this change. Perhaps that is better.
We can do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Thanks.
if not isinstance(body_nodes, list): | ||
body_nodes = [body_nodes] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in IFEXP
body is not a list.
Thanks for the comments! Now I have to remember what I wrote before! 😄 Will try to update the PR soon. |
@asottile made changes based on your review now. |
@asottile ping on this. |
I already approved, pyflakes (unfortunately) has multi-sign-off |
@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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, although I would personally default the for_annotations
to False
. Especially the test code would be a bit cleaner.
self.name = name | ||
self.source = source | ||
self.used = False | ||
assert isinstance(for_annotations, bool) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the purpose of this assert here?
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 😅 |
Great, let's merge it then! |
wellll now there's two open discussions that should probably be decided / resolved first |
Any updates on merging this PR? |
Howdy. Just a friendly reminder that is is sorely needed :) Can anyone rebase and merge ? Thanks. |
I will not be updating this PR. |
I haven't been following this PR in awhile, but I've been making updates to my PR #622 since we're using it at my work and I just updated it again now and saw this conversation. I believe it addresses all comments that were raised in this PR which was developed independently |
This PR finds a problem that has been observed in real code.
Consider the following code:
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.