typeguard icon indicating copy to clipboard operation
typeguard copied to clipboard

NameError on Enum

Open jolaf opened this issue 2 years ago • 20 comments

Things to check first

  • [X] I have searched the existing issues and didn't find my bug already reported there

  • [X] I have checked that my bug is still present in the latest release

Typeguard version

4.1.5

Python version

3.10.12

What happened?

Traceback (most recent call last):
  File "test.py", line 3, in <module>
    from A import A
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "/home/jolaf/.local/lib/python3.10/site-packages/typeguard/_importhook.py", line 98, in exec_module
    super().exec_module(module)
  File "A.py", line 7, in <module>
    A()
  File "A.py", line 5, in __init__
    def __init__(self, e: E = E.X) -> None:
NameError: name 'E' is not defined. Did you mean: 'e'?

How can we reproduce the bug?

test.py:


from typeguard import install_import_hook
with install_import_hook(('A',)):
    import A

A.py:

from enum import Enum
class A:
    class E(Enum):
        X = 1
    def __init__(self, e: E = E.X) -> None:
        print("OK")
A()
$ python3 A.py
OK

$ python3 test.py
<crash>

jolaf avatar Sep 11 '23 11:09 jolaf

Have you tried e: A.E?

agronholm avatar Sep 11 '23 12:09 agronholm

No I didn't, but as is it's ok with mypy.

jolaf avatar Sep 11 '23 13:09 jolaf

Mypy doesn't have to look up things at run time.

agronholm avatar Sep 11 '23 13:09 agronholm

That's true, but how do we decide whether typeguard should understand a particular annotation or not? My idea is we check whether mypy understands it, and if yes, than typeguard should too, if possible.

I don't think we should expect a user to change their code for typeguard to understand it.

jolaf avatar Sep 11 '23 16:09 jolaf

And if for some reason typeguard can't process some annotation correctly, at least it should not crash, preventing the app from running further.

jolaf avatar Sep 11 '23 16:09 jolaf

Have you tried e: A.E?

Now I have:

  File "A.py", line 5, in A
    def __init__(self, e: A.E = A.E.X) -> None:
NameError: name 'A' is not defined

jolaf avatar Sep 11 '23 19:09 jolaf

That should have been: def __init__(self, e: A.E = E.X) -> None: The reason for this is that the default gets assigned during the definition of the method, at which time A does not exist but E does! When that method is called, however, it's the opposite: A exists in the scope but E does not.

agronholm avatar Sep 11 '23 19:09 agronholm

That sounds extremely weird, and doesn't follow any guidelines on writing annotations and otherwise referencing variables. So I wouldn't expect someone to describe a method argument like this. :(

Also, it doesn't help:

  File "A.py", line 5, in A
    def __init__(self, e: A.E = E.X) -> None:
NameError: name 'A' is not defined

jolaf avatar Sep 11 '23 20:09 jolaf

It seems that mypy considers both E and A.E to be correct. So I think typeguard should ideally consider both of them to be correct. Or, if that's not possible, just skip the respective checks or issue a warning.

jolaf avatar Sep 11 '23 20:09 jolaf

It's not like Typeguard is considering E to be incorrect, it just doesn't have access to E in the scope of the call to __init__(). The only solution I can think of is doing what I did with __new__() in the other issue: aliasing E = A.E.

agronholm avatar Sep 11 '23 20:09 agronholm

Well, that sounds reasonable, isn't it?

jolaf avatar Sep 11 '23 20:09 jolaf

I just have a creeping feeling that I'm adding hacks on top of hacks and it's going to lead to more regressions down the line. But what else can I do? :shrug:

agronholm avatar Sep 11 '23 20:09 agronholm

Yeah, I understand that. :(

It seems like the only other thing possible is detecting situations like this and just skipping checking for them. But hacks are probably better.

jolaf avatar Sep 11 '23 20:09 jolaf

By the way, that example works if you add from __future__ import annotations or add quotes around A.E.

agronholm avatar Sep 11 '23 20:09 agronholm

I have a failing unit test for this now.

agronholm avatar Sep 11 '23 20:09 agronholm

Yep, but it works very weirdly:

$ python3 A.py 
OK

$ python3 test.py 
OK
OK

Why the extra instantiation?

jolaf avatar Sep 11 '23 20:09 jolaf

Does typeconfig.config.debug_instrumentation = True give any insight to that?

agronholm avatar Sep 11 '23 20:09 agronholm

You're instantiating A() both in test.py and A.py so this shouldn't be surprising.

agronholm avatar Sep 11 '23 20:09 agronholm

Oh, my bad. Instantiation is definitely not needed in test.py. Fixed.

jolaf avatar Sep 11 '23 20:09 jolaf

This problem is still actual for typeguard 4.3.0 and Python 3.12.3.

The error message is a bit different, though: NameError: name 'E' is not defined. Did you mean: 'self.E'?

jolaf avatar Sep 07 '24 23:09 jolaf