basedpyright icon indicating copy to clipboard operation
basedpyright copied to clipboard

import statement not respected in try/except statements

Open henri-gasc opened this issue 9 months ago • 8 comments

Description

You can find an simple example of the problem here, but a better example would be with the evdev module(where I found this), or other that have the same behavior.

To give a full explanation (just explaining the code in the library), I wanted to use the InputDevice class from evdev.device, which depends on a class named EventIO. This class is first imported from evdev.eventio_async, but if there is an ImportError, EventIO is imported from evdev.eventio.

The problem here, is that basedpyright only show completion for the attibutes and methods defined in evdev.device (for InputDevice) but not for its parent class EventIO.
(By the way, evdev.eventio_async's EventIO inherits evdev.eventio's EventIO; and the problem still happens even if it is an except: and not except ImportError:).

I think the problem comes from the import inside try/except block as using basedpyright on evdev.device shows that EventIO is unknow (despite being imported).

The playground example shows that the try: part of the block is not run because there is no reason for it to fail.
It seems like there is also the problem in pyright.

henri-gasc avatar Mar 09 '25 11:03 henri-gasc

The playground example shows that the try: part of the block is not run because there is no reason for it to fail.

this is because statically, there's no way for (based)pyright to know this. there's no way to tell the type checker that an exception definitely won't be raised, so it assumes that the except block is reachable.

ideally you'd be able to do something like this:

try:
    from pathlib import Path as _PathlibPath
    Path = _PathlibPath
except ImportError:
    class _FallbackPath:
        def __init__(self, val: str):
            self.val: str = val

    Path = _FallbackPath
    
class Foo(Path): ...

but this doesn't work either because it's essentially subtyping a union, which isn't allowed.

The problem here, is that basedpyright only show completion for the attibutes and methods defined in evdev.device (for InputDevice) but not for its parent class EventIO.

if you only care about completions and not type checking, maybe this could work for you?

from typing import Any


EventIO: Any
try:
    from .eventio_async import EventIO as _AsyncEventIO
    EventIO = _AsyncEventIO
except ImportError:
    from .eventio import EventIO as _EventIO
    EventIO = _EventIO

class InputDevice(EventIO):
    ...

DetachHead avatar Mar 09 '25 11:03 DetachHead

this is because statically, there's no way for (based)pyright to know this. there's no way to tell the type checker that an exception definitely won't be raised, so it assumes that the except block is reachable.

I understand this, but when using the evdev module, the except part is not reached (even if we change the except ImportError into an except

if you only care about completions and not type checking, maybe this could work for you?

I would (even if I do care about type checking), if this was not part of a module. While I could change my installed version of the module, this would not be great

henri-gasc avatar Mar 09 '25 13:03 henri-gasc

playing around with this some more, i can't seem to reproduce the issue you're describing with the EventIO class. completions from both the superclass and the subclass seem to work for me.

the minimal example you provided using Path on the other hand seems to behave differently because it's defining the class in the except block instead of importing it.

would you be able to provide an example of exactly how you're importing/using the EventIO class? either way this inconsistent behavior should be fixed, but i would just like to make sure we're on the same page so the fix actually addresses your use case.

DetachHead avatar Mar 16 '25 07:03 DetachHead

Sure! In a clean venv, using python 3.13.2, having done only pip install evdev, and changing the except ImportError: of .env/lib/python3.13/site-packages/evdev/device.py of line 9, to except:

from evdev import InputDevice
device = InputDevice(".")

device.leds() # Method from InputDevice, Warn about unused result
device.read_loop() # Method from EventIO, warning because 'Type of "read_loop" is unknown'

(And just to be sure, I also modified the system-wide installation, but it did not change anything)

henri-gasc avatar Mar 16 '25 08:03 henri-gasc

hmm i guess it's because of these errors:

try:
    from .eventio_async import EventIO
except ImportError:
    from .eventio import EventIO


# Argument to class must be a base class (reportGeneralTypeIssues)
# Base class type is unknown, obscuring type of derived class (reportUntypedBaseClass)
class InputDevice(EventIO): ... 

DetachHead avatar Mar 20 '25 15:03 DetachHead

Except that there is still the error with

try:
    from .eventio_async import EventIO
except:
    from .eventio import EventIO

henri-gasc avatar Mar 30 '25 17:03 henri-gasc

do you see a different error? i only see those two on the usage

DetachHead avatar Apr 01 '25 10:04 DetachHead

No, this is the same errors, but if

it assumes that the except block is reachable.

, then the errors should not be there having except:

Because it can not know what type (if there is even any) error statically, I understand why there is an error with

try:
    from .eventio_async import EventIO
except ImportError:
    from .eventio import EventIO

However, there should be no errors with the following code, as "it assumes the except block is reachable", and so EvenIO is defined, and is a base class.

try:
    from .eventio_async import EventIO
except:
    from .eventio import EventIO

henri-gasc avatar Apr 05 '25 09:04 henri-gasc