PyRight rightly complains when ExitStack is used:
with ExitStack() as stack:
for x in l:
stack.enter_context(x)
y = f()
print(y) # y is possibly unbound!
This is because there’s no way to know whether one of the context managers didn’t suppress an exception raised by another context manager.
How should this code be made safe?
You might try:
y = some_sentinel
with ExitStack() as stack:
for x in l:
stack.enter_context(x)
y = f()
assert y is not some_sentinel
print(y) # no error
But this gets annoying when annotating y, and when there are many variables. You might try adding a flag:
flag = False
with ExitStack() as stack:
for x in l:
stack.enter_context(x)
y = f()
flag = True
assert flag
print(y) # y is possibly unbound!
but this doesn’t fix the type error.
Would it make sense to add either a flag or a parameter to ExitStack that says that it cannot exit early? Either ExitStack(no_early_exit=True)
or ExitStack(on_early_exit=RuntimeError("blah"))
? Although I’m not sure how the annotation for ExitStack
would work to let type checkers know that __exit__
always return False
when that flag is given. I guess a final option would be to add something like:
In [15]: class SafeExitStack(ExitStack):
...: def __exit__(
...: self,
...: __exc_type: type[BaseException] | None,
...: __exc_value: BaseException | None,
...: __traceback: TracebackType | None
...: ) -> Literal[False]:
...: retval = super().__exit__(__exc_type, __exc_value, __traceback)
...: if retval:
...: raise RuntimeError("Internal error: suppressed excpeption")
...: return False
This would give a simple transformation of the above code to silence PyRight.