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

track if typing.TYPE_CHECKING to warn about non runtime bindings #622

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

terencehonles
Copy link
Contributor

When importing or defining values in if typing.TYPE_CHECKING blocks the bound names will not be available at runtime and may cause errors when used in the following way:

import typing

if typing.TYPE_CHECKING:
    from module import Type  # some slow import or circular reference

def method(value) -> Type:  # the import is needed by the type checker
    assert isinstance(value, Type)  # this is a runtime error

This change allows pyflakes to track what names are bound for runtime use, and allows it to warn when a non runtime name is used in a runtime context.

tox.ini Outdated Show resolved Hide resolved
@asottile
Copy link
Member

#530 predates this and has similar goals / problems -- perhaps collaborate with that PR?

@terencehonles terencehonles force-pushed the warn-about-using-type-checking-imports-at-runtime branch from ac90967 to a795822 Compare March 23, 2021 22:18
@terencehonles
Copy link
Contributor Author

terencehonles commented Mar 23, 2021

#530 predates this and has similar goals / problems -- perhaps collaborate with that PR?

@asottile It looks like I actually ended up writing about the same code 😅 however there are a few things I have which the other one doesn't:

  1. The logic handles the non runtime bindings in either the body or the else of the if/else expression
  2. The runtime attribute defaults to True (it generally will be) which needs less changes in other parts of the codebase.
  3. The lines https://github.com/PyCQA/pyflakes/pull/530/files#diff-5df44d79406311387e0056faae7a8092d1ce9cc85c7d15420186e45a0c712283R1178-R1181 continues looking up the stack for a runtime value, but it's more likely that the non runtime value will be declared at a higher scope and therefore the code I wrote warns and aborts immediately.

It also looks like the code was waiting review for almost a year. Is there something you wanted changed from that PR? I'm willing to work with @PetterS, but I didn't realize there was an PR that old for this already opened.

@asottile
Copy link
Member

ah shoot, that's probably on me -- I might've missed the last round of reviews on there and then it lapsed (and now conflicts)

@PetterS
Copy link
Contributor

PetterS commented Mar 24, 2021

I can revive that PR if we agree that it it is likely to be merged.

This is a problem that has been observed for real code, so would be good to fix.

@terencehonles
Copy link
Contributor Author

I can revive that PR if we agree that it it is likely to be merged.

This is a problem that has been observed for real code, so would be good to fix.

It should be pretty easy to xref this PR in order to see what might also need to change since I based my PR off the most recent release (I was not aware of your PR when starting). We're 99% inline with each other's implementations.

@PetterS
Copy link
Contributor

PetterS commented Mar 25, 2021

ah shoot, that's probably on me -- I might've missed the last round of reviews on there and then it lapsed (and now conflicts)

Merged it with master again now.

@terencehonles terencehonles force-pushed the warn-about-using-type-checking-imports-at-runtime branch from a795822 to 117d5de Compare April 12, 2021 23:21
@terencehonles terencehonles force-pushed the warn-about-using-type-checking-imports-at-runtime branch from 117d5de to 3bbd41a Compare October 18, 2021 14:43
@terencehonles terencehonles force-pushed the warn-about-using-type-checking-imports-at-runtime branch from 3bbd41a to 53c3070 Compare August 11, 2022 11:12
@terencehonles terencehonles force-pushed the warn-about-using-type-checking-imports-at-runtime branch from 53c3070 to 58506b3 Compare March 31, 2023 10:58
@terencehonles terencehonles force-pushed the warn-about-using-type-checking-imports-at-runtime branch from 58506b3 to 6dcf6a7 Compare September 27, 2023 11:36
@terencehonles
Copy link
Contributor Author

#530 predates this and has similar goals / problems -- perhaps collaborate with that PR?

@asottile since the PR in comment above is not going to be updated by its author

#530 (comment)

any chance you'd be willing to review this PR?

As mentioned above #622 (comment) it had used negated logic compared to the other PR in order to minimize the number of changes to the code base, and I've continued to both use and update this PR for changes on the development branch.

I can seek a 2nd approver if we can move forward with this PR.

Copy link
Member

@asottile asottile left a comment

Choose a reason for hiding this comment

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

I don't think the tests cover all the cases you intend to demonstrate here

Comment on lines +2058 to +2059
ClassDefinition(
node.name, node, runtime=not self._in_type_check_guard))
Copy link
Member

Choose a reason for hiding this comment

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

if you're going to reindent things can you use this style? https://github.com/asottile/add-trailing-comma#multi-line-method-invocation-style----why

alternatively I could reformat the codebase and the you'd deal with a round of merge conflicts -- either way

Comment on lines 734 to +737
nodeDepth = 0
offset = None
_in_annotation = AnnotationState.NONE
_in_type_check_guard = False
Copy link
Member

Choose a reason for hiding this comment

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

these are actually all incorrect -- should be assigned in __init__ since they are not class vars

if (not n.runtime and not (
self._in_type_check_guard
or self._in_annotation)):
self.report(messages.UndefinedName, node, name)
Copy link
Member

Choose a reason for hiding this comment

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

should we make a separate type for this (so the message can clarify "only something something type checking"?)

@@ -1073,12 +1078,18 @@ def handleNodeLoad(self, node, parent):
self.report(messages.InvalidPrintSyntax, node)

try:
Copy link
Member

Choose a reason for hiding this comment

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

this try is now way too broad -- it originally only guarded the name lookup but now has a whole bunch of unrelated code in it

@@ -226,10 +226,11 @@ class Binding:
the node that this binding was last used.
"""

def __init__(self, name, source):
def __init__(self, name, source, runtime=True):
Copy link
Member

Choose a reason for hiding this comment

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

this boolean-trap should be keyword only -- same for all the other places it is introduced

Copy link
Member

Choose a reason for hiding this comment

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

see https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/

This should at least be , *, runtime=True

However as noted earlier in reviews, runtime isnt a good name. I would prefer to see a more explicit in_type_checking or similar, and have one name consistently used everywhere.

Also I suspect that this feature will be a lot cleaner if TYPE_CHECKING is treated as a special scope, which automatically means items in it are not placed into the normal scope logic. Then items in that special scope can only be considered by explicitly including them, only in the contexts when they are useful.

# normalize body and orelse to a list
body, orelse = (
i if isinstance(i, list) else [i]
for i in (node.body, node.orelse))
Copy link
Member

Choose a reason for hiding this comment

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

i is a bad name here -- i should be for incrementing integers only

Copy link
Member

Choose a reason for hiding this comment

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

also this is a very strange way to do this -- don't go through a generator for two assignments just do the two assignments


for n in orelse:
self.handleNode(n, node)
self._in_type_check_guard = orig
Copy link
Member

Choose a reason for hiding this comment

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

this should be a context manager or a try: finally: -- as it is right now it is not exception safe


# set the guard and handle the orelse
if type_checking:
self._in_type_check_guard = True if reverse else orig
Copy link
Member

Choose a reason for hiding this comment

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

True if reverse else orig is better written as reverse or orig -- any time True if or False if should set off alarm bells

Comment on lines +1824 to +1830
# check if the body/orelse should be handled specially because it is
# a if TYPE_CHECKING guard.
test = node.test
reverse = False
if isinstance(test, ast.UnaryOp) and isinstance(test.op, ast.Not):
test = test.operand
reverse = True
Copy link
Member

Choose a reason for hiding this comment

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

I'd rather see a big if statement than a bunch of locals introduced here

@@ -2038,12 +2089,15 @@ def TUPLE(self, node):
LIST = TUPLE

def IMPORT(self, node):
runtime = not self._in_type_check_guard
Copy link
Member

Choose a reason for hiding this comment

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

if we're going to invert this maybe it should just be _runtime ? but then that's not a great name so maybe runtime is not a great name?

@terencehonles
Copy link
Contributor Author

Thanks for the review, I'll see when I have time to address it.

When importing or defining values in ``if typing.TYPE_CHECKING`` blocks
the bound names will not be available at runtime and may cause errors
when used in the following way::

  import typing

  if typing.TYPE_CHECKING:
    from module import Type  # some slow import or circular reference

  def method(value) -> Type:  # the import is needed by the type checker
    assert isinstance(value, Type)  # this is a runtime error

This change allows pyflakes to track what names are bound for runtime
use, and allows it to warn when a non runtime name is used in a runtime
context.
@terencehonles terencehonles force-pushed the warn-about-using-type-checking-imports-at-runtime branch from 6dcf6a7 to 8c4da4a Compare February 9, 2024 09:38
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.

5 participants