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
- Open serial port
- Switch the device to the remote control mode
- Teardown
- Turn off the voltage
- Switch the device to the local control mode
- 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?
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
finallyclauses. 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.