What if __enter__ and __exit__ raises?

66 Views Asked by At

I'm working on a simple package to control my power supply remotely. I wrapped the logic in a class, let's call it PowerSupply. The communication is over a serial port. Also before actual control a specific command should be transferred in order to switch the device to the remote mode. There is also command to revert it back to the local mode. So here is the description of the context manager I would like to use:

  • Setup
    1. Open serial port
    2. Switch the device to the remote control mode
  • Teardown
    1. Turn off the voltage
    2. Switch the device to the local control mode
    3. Close serial port

The problem is (almost) each of the above points can raise actually. Here is very simplified code :

class PowerSupply :
    def __init__(self, port: str) :
        self._device = serial.Serial(port)

    def __enter__(self) :
        try :
            self.setMode(EMode.REMOTE)
        except Exception as outer :
            try :
                self.__exit__(*sys.exc_info()) # should be cleaned anyway
            except Exception as inner :
                pass
            finally :
                raise outer # we don't want to miss this error
        else :
            return self

    def __exit__(self, _, __, ___) :
        try :
            self.setOut(Out.OFF)
            self.setMode(EMode.LOCAL)
        except Exception as e :
            raise e # we don't want to miss this error
        finally :
            self.close()

    def setMode(...) :
        pass # can raise

    def setOut(...) :
        pass # can raise

The usage is straightforward:

with PowerSupply("/dev/ttyUSB0") as ps :
    ps.setMaxVoltage(5000);
    ps.setMaxCurrent(1000);
    ps.setVoltage(1000);
    ps.setOut(EOut.ON);
    # work

I see one problem (excluding the code full of trys) with this approach: __exit__ replaces the exception from the with body. Actually, it is not quite a problem in my case because I made every single command to be logged and any error is also logged internally. But anyway, could be a problem in other projects.

What is a recommended approach to deal with such kind of cases when both setup and teardown raise?

1

There are 1 best solutions below

1
jsbueno On

Python does preserve the TB info of all chained exceptions, there is mostly no need to do anything, but to ensure you run your tear-down code in the finally clauses. Just as @KamilCuk puts in the comment above. Previous exceptions are chained by setting the __context__ attribute in the last raised exception - the default traceback print handles that nicely.

If at any point you know that one exception caused the other downstream, during handling, you may use the raise ... from ... syntax - that changes the message to your user to "this exception was the direct cause of this next exception" instead of the default "during handling of this previous exception this following exception was raised". (The context in this case is preserved in the Exception's __cause__ attribute instead)

What might be nice is to factor-out the __exit__ method to simplify the understanding of your flows.


class PowerSupply :
    def __init__(self, port: str) :
        self._device = serial.Serial(port)

    def __enter__(self) :
        try :
            self.setMode(EMode.REMOTE)
        except Exception as outer :
            #try :
                self._inner_exit() 
            #except Exception as inner : # redundant block - Python will add the "outer" exception as a context for "inner"
                #raise inner 
            #finally : # redundant  - just let the exception buble up
                #raise outer # we don't want to miss this error
        else :
            return self
        
    def _inner_exit(self):
        try :
            self.setOut(Out.OFF)
            self.setMode(EMode.LOCAL)
        #except Exception as e :  # redundant block: any exception will propagate
        #    raise e # we don't want to miss this error
        finally :
            self.close()
        

    def __exit__(self, exctype, exc, tb) :
        #try :
            aelf._inner_exit()
        #except Exception as e : # redundant block: "exc" is added as context for the inner "e" exception
            #raise e from exc  # attach  expertise/tooling to follow).