Created
June 13, 2021 17:05
-
-
Save muppetjones/4b964e1a57447e31476f41487d641cb2 to your computer and use it in GitHub Desktop.
Edge Case for SystemExit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""Example SystemExit edge case. | |
Try:except blocks should always be as explicit as possible about the errors | |
the catch; however, sometimes you really just need to catch anything. When | |
this "anything" happens, always catch "Exception" -- never use a bare except. | |
"But a bare except is the only way to catch everything", you say? | |
Nope. You just need to be more explicit: SystemExit, KeyboardInterrupt, | |
and GeneratorExit are a bit more slippery and will not be caught with | |
"Exception", but can be caught explicitly. | |
If another error occurs while you're handling the first, the second error | |
will be placed on the top of the stack, and the first error will not reslove. | |
This funcionality makes sense -- you've probably seen it via the message | |
"During handling of the above exception, another exception occurred:" -- but it | |
can lead to a fun edge case with the used with "finally": If a SystemExit is | |
raised in "try", then another exception is raised during a "finally" | |
block, the SystemExit will be silent. | |
Example: | |
# If "func" raises a SystemExit or similar, the logger call in the "finally" | |
# block will raise an UnboundLocalError that will replace SystemExit on | |
# the traceback handling stack. | |
# If this block is also handled via "except Exception", the SystemExit | |
# will be lost. | |
try: | |
try: | |
val = func() # func raises SystemExit | |
except Exception: | |
val = None | |
finally: | |
logger.info('got %s', val) # Will raise UnboundLocalError | |
except Exception: | |
logger.info('this will be logged!') # And our handling is resolved! | |
except SystemExit: | |
logger.info('this will never be called') | |
Example: | |
$ ./sandbox_sys_exit.py | |
INFO === Calling primary function: <function raise_system_exit at 0x10a2534c0> | |
INFO -- Caught slippery err: SystemExit | |
INFO -- Calling secondary function: <function raise_for_secondary at 0x10a003160> | |
INFO >>> Caught secondary | |
... | |
""" | |
import functools | |
import logging | |
import sys | |
from typing import Callable, Optional | |
FORMAT = '%(levelname)-8s %(message)s' | |
logging.basicConfig(format=FORMAT, level=logging.DEBUG) | |
LOGGER = logging.getLogger('sandbox-sys-exit') | |
# Raise Functions | |
# ====================================================================== | |
def do_nothing(): | |
... | |
def raise_for_primary(): | |
raise ValueError('primary') | |
def raise_for_secondary(): | |
raise Exception('secondary') | |
def raise_for_tertiary(): | |
raise Exception('tertiary') | |
def raise_generator_exit(): | |
raise GeneratorExit('GeneratorExit') | |
def raise_keyboard_interrupt(): | |
raise KeyboardInterrupt('KeyboardInterrupt') | |
def raise_system_exit(): | |
raise SystemExit('SystemExit') | |
# Example | |
# ====================================================================== | |
def example( | |
primary: Callable, | |
secondary: Optional[Callable] = None, | |
tertiary: Optional[Callable] = None, | |
logger: logging.Logger = LOGGER, | |
) -> None: | |
"""Demonstrate edge case for try:except:finally block. | |
Arguments: | |
primary: A callable that raises an exception -- preferably one of | |
SystemExit, KeyboardInterrupt, or GeneratorExit. | |
secondary: An optional callable for use in "except" blocks. Should raise | |
any other exception. If not given, we won't do anything. | |
tertiary: An optional callable for use in "finally" block. Should raise | |
any other exception. If not given, we won't do anything. | |
logger: An optional logger. | |
Returns: | |
None. | |
Raises: | |
Whatever you tell it to. | |
""" | |
secondary = secondary or do_nothing | |
tertiary = tertiary or do_nothing | |
try: | |
logger.info('=== Calling primary function: %s', primary) | |
primary() | |
except Exception as err: | |
logger.info(' -- Caught regular err: %s', err) | |
secondary() | |
raise | |
except (SystemExit, KeyboardInterrupt, GeneratorExit) as err: | |
logger.info(' -- Caught slippery err: %s', err) | |
secondary() | |
raise | |
else: | |
logger.info(' -- No error. This is boring.') | |
finally: | |
logger.info(' -- Calling secondary function: %s', secondary) | |
tertiary() | |
return | |
# Main, etc. | |
# ====================================================================== | |
def main(logger: logging.Logger = LOGGER): | |
"""Entrypoint.""" | |
primary = [ | |
raise_for_primary, | |
raise_system_exit, | |
raise_keyboard_interrupt, | |
raise_generator_exit, | |
functools.partial(sys.exit, 0), | |
functools.partial(sys.exit, 1), | |
] | |
# An error during error handling | |
# ---------------------------------- | |
secondary = raise_for_secondary | |
tertiary = None | |
for func in primary: | |
try: | |
example(func, secondary, tertiary) | |
except (Exception, SystemExit, KeyboardInterrupt, GeneratorExit) as err: | |
logger.info('>>> Caught %s', err) | |
else: | |
logger.info('>>> Nothing raised') | |
# Another error during "finally" | |
# ---------------------------------- | |
secondary = None | |
tertiary = raise_for_tertiary | |
for func in primary: | |
try: | |
example(func, secondary, tertiary) | |
except (Exception, SystemExit, KeyboardInterrupt, GeneratorExit) as err: | |
logger.info('>>> Caught %s', err) | |
else: | |
logger.info('>>> Nothing raised') | |
if __name__ == '__main__': | |
main() | |
# __END__ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment