I'd like to show you two versions of my two functions that support my logging tools. Their purpose is to log a different message (here abort
) when an error occured and it matches the filter specified by the user. By default the error
message is logged.
Version-1 of this code uses the classical if
to handle the optional filter, while version-2 uses the a try/except
.
Classical if
The first function is an optional filter that logs an abort
trace when the exception matches any of the error types specified by the user and returns True
in this case, otherwise False
:
class LogAbortWhen:
def __init__(self, *error_types: type[Exception]):
self.error_types = error_types
def __call__(self, exc: Exception, logger: TraceLogger) -> bool:
if any((isinstance(exc, t) for t in self.error_types)):
logger.final.log_abort(message=f"Unable to complete due to <{type(exc).__name__}>: {str(exc) or '<N/A>'}")
return True
return False
The other one logs the default error
trace when there is no custom filter or the filter returned False
:
@contextlib.contextmanager
def telemetry_context(
activity: str,
source: Source,
on_error: Optional[OnError] = None,
) -> ContextManager[TraceLogger]: # noqa
parent = current_logger.get()
logger = BasicLogger(activity)
tracer = TraceLogger(logger)
tracer.initial.source.append(source)
token = current_logger.set(Node(logger, parent))
try:
yield tracer
except Exception as e: # noqa
if not on_error or not on_error(e, tracer):
tracer.final.log_error(message=f"Unhandled <{type(e).__name__}> has occurred: <{str(e) or 'N/A'}>")
raise
finally:
current_logger.reset(token)
Easier to ask for forgiveness than permission: try/except
But then I thought, how about rewriting it with the easier to ask for forgiveness than permission principle in mind, so I did the following (theoretical code):
The LogAbortWhen2
now throws the DoesNotApply
error when it didn't do anything:
class DoesNotApply(Exception):
pass
class LogAbortWhen2:
def __init__(self, *error_types: type[Exception]):
self.error_types = error_types
def __call__(self, exc: Exception, logger: TraceLogger):
if any((isinstance(exc, t) for t in self.error_types)):
logger.final.log_abort(message=f"Unable to complete due to <{type(exc).__name__}>: {str(exc) or '<N/A>'}")
else:
raise DoesNotApply()
And the context-manager telemetry_context2
doesn't check the return value anymore, but handles the two errors that may occur: TypeError | DoesNotApply
. (the TypeError
is for when the on_error
variable is None
; I know, I should implement an empty empty filter always throwing the DoesNotApply
error)
@contextlib.contextmanager
def telemetry_context2(
activity: str,
source: Source,
on_error: Optional[OnError] = None,
) -> ContextManager[TraceLogger]: # noqa
parent = current_logger.get()
logger = BasicLogger(activity)
tracer = TraceLogger(logger)
tracer.initial.source.append(source)
token = current_logger.set(Node(logger, parent))
try:
yield tracer
except Exception as e: # noqa
try:
on_error(e, tracer)
except TypeError | DoesNotApply:
tracer.final.log_error(message=f"Unhandled <{type(e).__name__}> has occurred: <{str(e) or 'N/A'}>")
raise
finally:
current_logger.reset(token)
Which version would you prefer? The first one with the bool
return value and the additional if
or the second one with the try/except
? Also how strict are you about the EAFP principle? Do you often write your code if
-free and favor try/except
s?