-
Notifications
You must be signed in to change notification settings - Fork 114
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
Quoted annotations do not work in cyclic imports #70
Comments
The reasons for this happening are pretty evident, and the following approaches to resolving the issue seem reasonable:
|
I have explained in the README how to avoid this situation. Import the module, not the object inside it. Then use an annotation like |
How do you propose that I accomplish this without creating a typed syntax tree of each module? |
Well, probably this particular approach was not a very good idea. :) |
I tried that, it doesn't change the situation effectively. |
Why not? It lets you avoid the errors from the circular imports, doesn't it? You'd have to forego using |
Unfortunately it doesn't help to avoid errors from circular imports. |
I am stumped. Can you provide a minimum runnable example with two modules that have an unfixable circular import issue? |
I consider the one in the issue a good example of that. Here's the version without using A.py: from typeguard import TypeChecker
import B
class A:
def f(self, b: B.B) -> None:
b.f(self)
with TypeChecker(('__main__', 'A', 'B')):
A().f(B.B()) B.py: from typing import TYPE_CHECKING
if TYPE_CHECKING:
import A
class B:
def f(self, a: 'A.A') -> None:
print("OK") Produces this: $ python3 A.py
/usr/local/lib/python3.6/dist-packages/typeguard/__init__.py:657: UserWarning: the system profiling hook has changed unexpectedly
warn('the system profiling hook has changed unexpectedly')
Traceback (most recent call last):
File "A.py", line 10, in <module>
A().f(B.B())
File "A.py", line 7, in f
b.f(self)
File "/home/vmz/git/userfe/B.py", line 7, in f
def f(self, a: 'A.A') -> None:
File "/usr/local/lib/python3.6/dist-packages/typeguard/__init__.py", line 690, in __call__
memo = self._call_memos[frame] = _CallMemo(func, frame)
File "/usr/local/lib/python3.6/dist-packages/typeguard/__init__.py", line 55, in __init__
hints = get_type_hints(func)
File "/usr/lib/python3.6/typing.py", line 1543, in get_type_hints
value = _eval_type(value, globalns, localns)
File "/usr/lib/python3.6/typing.py", line 350, in _eval_type
return t._eval_type(globalns, localns)
File "/usr/lib/python3.6/typing.py", line 245, in _eval_type
eval(self.__forward_code__, globalns, localns),
File "<string>", line 1, in <module>
NameError: name 'A' is not defined It starts working if I remove Maybe I miss something. |
The problem is that you still have |
And replace the the annotation in the A module with the string equivalent. |
If you're running Python 3.7 you can also add |
For now I can only use 3.6 unfortunately. Here it goes: A.py: from typeguard import TypeChecker
import B
class A:
def f(self, b: 'B.B') -> None:
b.f(self)
with TypeChecker(('__main__', 'A', 'B')):
A().f(B.B()) B.py: import A
class B:
def f(self, a: 'A.A') -> None:
print("OK") Produces this even with $ python3 A.py
Traceback (most recent call last):
File "A.py", line 3, in <module>
import B
File "/home/vmz/git/userfe/B.py", line 1, in <module>
import A
File "/home/vmz/git/userfe/A.py", line 10, in <module>
A().f(B.B())
AttributeError: module 'B' has no attribute 'B' |
The problem now is the A module. Guard the entry point with |
Actually I'm not sure about that. Moving the entry point to a separate module would be your best bet. The reason being that now there are two versions of the A class: one in the |
Yep, it is: $ python3 A.py
/usr/local/lib/python3.6/dist-packages/typeguard/__init__.py:694: TypeWarning: [MainThread] call to B.B.f() from A.py:7: type of argument "a" must be A.A; got __main__.A instead
warn(TypeWarning(memo, event, frame, exc))
OK |
This worked: a.py: import b
class A:
def f(self, x: 'b.B') -> None:
x.f(self) b.py: import a
class B:
def f(self, x: 'a.A') -> None:
print("OK") c.py: import b
from a import A
from typeguard import TypeChecker
with TypeChecker(('__main__', 'a', 'b')):
A().f(b.B()) |
Well, I see your point. Unfortunately, I have a rather large code base authored by many people, and there's a lot of situations like this there. Of course I can invest in refactoring all the code for it to be usable with The Python itself handles the problem perfectly well - the problem of circular dependencies exists only when defining new things, otherwise you can use any object in any module without importing it's class, passing the reference is enough. When we started annotating our code to use But now it seems that to use It seems to me that for a code analysis tool requiring such heavy modifications to the code being analyzed is not very desirable. I understand that it may be not easy to fix, so all I ask is not disregard this issue as seemingly non-important. Thank you! |
Unfortunately I'm not sure that any of the proposed solutions are good enough for me to integrate to typeguard, at least by default. Let's review your proposals one by one:
|
Lot of warnings is not a problem as they're easily filtered by For the way No.3 - so, For now instead, I have to either refactor the code permanently, or temporary replace all problematic annotations with And I certainly agree that such shortcuts are better as configurable options, and not as default behavior. |
If I had to choose, I'd go for option 1 as an opt-in setting. |
Thinking about this more, I could actually implement both as a policy setting on TypeChecker. |
Both sounds really good. Thank you very much! |
I still need to document this but the README is getting kinda long. I need to consider building actual Sphinx documentation. |
Cool, thanks! |
I am having the same issue as @jolaf , but in my case my project entrypoint uses from typeguard.importhook import install_import_hook
install_import_hook("my_project")
from my_project.this import do_this
from my_project.that import do_that
if __name__ == "__main__":
do_this()
do_that() |
Is there a way to make what work? Elaborate. |
Sorry about that, I just tried to deal with this issue for enough time to make it obvious to myself and forgot to add context. with TypeChecker(('__main__', 'A', 'B')):
A().f(B()) to with TypeChecker(('__main__', 'A', 'B'), forward_refs_policy=ForwardRefPolicy.WARN):
A().f(B()) fixes the issue. Now, my project uses |
A minimal working example for my situation would be this:
from typeguard.importhook import install_import_hook
install_import_hook("B")
from B import B
class A:
def f(self, b: B) -> None:
b.f(self)
if __name__ == "__main__":
A().f(B())
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from A import A
class B:
def f(self, a: "A") -> None:
print("OK") |
Couldn't you just import the modules and annotate like |
As for OP, I'm working on a large codebase, which makes refactoring each occurrence not feasible. Given that the sample are fully Python (code and type hints) compliant, I would suggest having a way to ignore or avoid failure on such occasions, similar to the workaround implemented for the TypeChecker class. |
The best I could do is to not touch |
Surely it would be better than having to not use it, assuming it's something that can be toggled. In that way the user has to actively allow typeguard to ignore some type hints |
I think I understand the problem a bit better now. It's not |
Yes exactly, since I know that the annotations make sense I would like typeguard to ignore the errors, as with |
Any update on this? |
No (see #198). |
Python usually doesn't allow cyclic imports, but they're sometimes necessary for proper static type annotations. When that is the case, the cyclic import is guarded by
if TYPE_CHECKING:
and annotations for types imported from such a module are quoted like strings to avoid errors at runtime. That's a standard practice recommended bymypy
developers.However, any program that uses this approach to annotations seems incompatible with
typeguard
.Consider this example:
A.py:
B.py:
If
typeguard
is disabled, the program performs fairly well as it is in fact absolutely correct in runtime, and also correct frommypy
point of view:However if it is run with
typeguard
, the following fatal exception occurs:The text was updated successfully, but these errors were encountered: