Features
Main Features
There are a few main features of py-buzz
that are noteworthy:
Raise exception on condition failure
The py-buzz
package provides the require_condition()
function that checks a
condition and raises an exception if it fails. This is nice, because you often
find your self writing a lot of if <whatever>: raise Exception(<message>)
throughout your code base. It's just a little easier with py-buzz
:
# Vanilla python
if not some_condition():
raise Exception("some_condition failed")
# With py-buzz
require_condition(some_condition(), "some_condition failed")
This is mostly just a bit of syntactic sugar, but it can make your code a bit more palatable. This is especially true in functions that need to check a lot of conditions before prior to executing their core logic.
You may also specify the exception type that should be raised by passing it to the
raise_exc_class
parameter:
require_condition(
some_condition(),
"some_condition failed",
raise_exc_class=MyProjectError,
)
In this case, a MyProjectError
would be raised if the condition fails.
There are a few special keyword argument parameters for the require_condition()
function:
raise_exc_class
This just specifies the type of exception to raise if the condition fails.
It defaults to Exception
.
raise_args
With this parameter, you can specify any positional arguments that should be passed to the raised exception after the message. Here is an example:
class MyProjectError(Exception):
def __init__(self, message, init_arg1: str, init_arg2: str):
self.init_arg1 = init_arg1
self.init_arg2 = init_arg2
require_condition(
some_condition(),
"some_condition failed",
raise_exc_class=MyProjectError,
raise_args=["foo", "bar"],
)
If the condition fails, require_condition()
will rais a MyProjectError
initialized
with positional args init_arg1 == "foo"
and init_arg2 == "bar"
.
raise_kwargs
Like the raise_args
parameter, this one passes along a dictionary of keyword arguments
to the newly raised exception:
class MyProjectError(Exception):
def __init__(self, message, init_kwarg1: str | None = None, init_kwarg2: str | None = None):
self.init_kwarg1 = init_kwarg1
self.init_kwarg2 = init_kwarg2
require_condition(
some_condition(),
"some_condition failed",
raise_exc_class=MyProjectError,
raise_kwargs=dict("foo", "bar"),
)
If the condition fails, require_condition()
will rais a MyProjectError
initialized
with keyword arguments init_kwarg1 == "foo"
and init_kwarg2 == "bar"
.
exc_builder
If the exception to be raised with the raise_exc_class
option is a non-standard
exception type that does not take a string message as its first argument, you will need
to use an alternative exception builder that knows how to map the exception parts to the
correct place.
For example, FastAPI's HTTPException
takes a
status_code
as its first positional argument and expects that any message details are
passed as a keyword argument named details
.
In this case, you need to define a builder function to construct the exception and pass
it to the exc_builder
option. The exc_builder
option should be a callable function
that accepts a single parameter of type ExcBuilderParams
that can be imported from
buzz
.
class WeirdArgsError(Exception):
def __init__(self, *args, detail: str = "", **kwargs):
self.detail = detail
super().__init__(*args, **kwargs)
def weird_builder(params):
return exc_class(*params.raise_args, detail=params.message, **params.raise_kwargs)
require_condition(
some_condition(),
"some_condition failed",
raise_exc_class=WeirdArgsError,
raise_args=["jawa"],
raise_kwargs=dict("ewok"="hutt")
exc_builder=weird_builder,
)
do_except
Sometimes, it is useful to do some particular things when a condition is not met. A
good example would be to log that the condition failed. The do_except
optional argument
provides the ability to do this. The do_except
option should be a callable function
that accepts a single parameter that is the exception that will be raised.
This option might be invoked something like this:
def log_error(exc: Exception):
logger.error(f"The condition failed: {exc}")
require_condition(
some_condition(),
"some_condition failed",
do_except=log_error,
)
do_else
This option describes some action that should happen if the condition passes.
This option is less useful than do_except
but it may useful in some circumstances.
This option should be a callable that takes no arguments:
def log_yay():
logger.info("it's all good!")
require_condition(
some_condition(),
"some_condition failed",
do_else=log_yay,
)
Raise exception if value is not defined
The py-buzz
package provides the enforce_defined()
function that checks a
value and raises an exception if it is not defined. This is especially useful
for both checking if a variable passed to a function is defined and also to
satisfy static type checkers when you want to call a method on the object.
# Vanilla python
def vanilla(val: str | None) -> str:
if val is None:
raise Exception("Received an undefined value!")
return val.upper()
# With py-buzz
def buzzy(val: str | None) -> str:
return enforce_defined(val).upper()
This is also mostly just syntactic sugar, but it can save you a few lines of code and is still very expressive. It might also be useful if you need to supply some more context in your error:
def neopolitan(val: str | None):
return enforce_defined(
val,
"Received an undefined value!"
raise_exc_class=MyProjectError,
raise_args=["jawa", "ewok"],
raise_kwargs=dict(hutt="pyke"),
)
In this case, a MyProjectError
with be raised with positional arguments of "jawa"
and
"ewok"
and a keyword argument of hutt="pyke"
if the value passed in is not defined.
By default, enforce_defined()
raises an exception with a basic message saying that the
value was not defined. However, you may pass in a custom message with the message
keyword argument.
The enforce_defined()
function also accepts some keyword arguments:
raise_exc_class
Functions the same as require_condition()
.
raise_args
Functions the same as require_condition()
.
raise_kwargs
Functions the same as require_condition()
.
exc_builder
Functions the same as require_condition()
.
do_except
Functions the same as require_condition()
.
do_else
Functions the same as require_condition()
.
Raise exception if value is wrong type
The py-buzz
package also provides the ensure_type()
function that checks a
value and raises an exception if it is not of the expected type. This function
serves a dual purpose: it will raise an error if your value is the wrong type,
and it will also narrow the type for you to make your type-check happy.
# Vanilla python
def vanilla(val: str | int) -> str:
if not isinstance(val, str):
raise Exception("Received a non-string value!")
return val.upper()
# With py-buzz
def buzzy(val: str | int) -> str:
return ensure_type(val, str).upper()
Though this could be called syntactic sugar as well, it saves some keystrokes and is an elegant way to narrow your type. At the same time, it is a nice approach to applying negativ-space-programming because it doesn't just "cast" the type, it raises an exception if the type doesn't match. It can also need to supply some more context in your error:
def neopolitan(val: str | int):
return ensure_type(
val,
str,
"Received a non-string value!"
raise_exc_class=MyProjectError,
raise_args=["jawa", "ewok"],
raise_kwargs=dict(hutt="pyke"),
)
In this case, a MyProjectError
with be raised with positional arguments of "jawa"
and
"ewok"
and a keyword argument of hutt="pyke"
if the value passed in is not str
.
By default, ensure_type()
raises an exception with a basic message saying that the
value was the wrong type. However, you may pass in a custom message with the message
keyword argument.
The ensure_type()
function also accepts some keyword arguments:
raise_exc_class
Functions the same as require_condition()
.
raise_args
Functions the same as require_condition()
.
raise_kwargs
Functions the same as require_condition()
.
exc_builder
Functions the same as require_condition()
.
do_except
Functions the same as require_condition()
.
do_else
Functions the same as require_condition()
.
Exception handling context manager
The py-buzz
package provides the handle_errors()
context manager that
catches any exceptions that might be raised while executing a bit of code. The
caught exceptions are re-packaged and raised as another exception type. The
message attached to the new expression captures the initial exception's
message:
# Vanilla python
try:
this_could_fail()
this_could_also_fail()
this_will_definitely_fail()
except Exception as err:
raise RuntimeError(f"Something didn't work -- Error: {err}")
# With py-buzz
with handle_errors("Something didn't work", raise_exc_class=RuntimeError):
this_could_fail()
this_could_also_fail()
this_will_definitely_fail()
This actually can save a bit of code and makes things a bit cleaner. It is also a
implements pattern that tends to get repeated over and over again. If you need to do
very complicated things while handling an exception, you should use a standard try-
catch block. However, there are some extra bells and whistles on handle_errors()
that
can be used by passing additional keyword arguments to the function.
raise_exc_class
This parameter is the same as for require_condition()
. However, if you pass None
it
will not raise a new exception. Instead, handle_errors
will process the do_except
,
do_else
, and do_finally
methods and then continue. This effectively absorbs any
exceptions that occur in the context. However the context is immediately exited after
the first raised exception.
raise_args
Functions the same as require_condition()
.
raise_kwargs
Functions the same as require_condition()
.
exc_builder
Functions the same as require_condition()
.
handle_exc_class
This option describes the type of exception that will be handled by this context manager. Any instance of the option's exception (or any of its derived exception classes) will be caught. This is very useful if you only want to handle a certain category of exceptions and let the others rise up un-altered:
with handle_errors("Something went wrong", handle_exc_class=MyProjectError):
some_function_that_could_raise_mine_or_other_errors()
Exception instances that do not fall within the inheritance tree of the
handle_exc_class
option will not be handled at all. It is worth noting that the
do_except
task will not be executed if another exception type occurs. However,
the do_else
and do_finally
tasks will be executed normally.
ignore_exc_class
This option describes a type of exception that should not be handled by this
context manager. Any instance of the option's exception (or any of its derived
exception classes) will be raised immediately by handle_errors
and will be not
be handled or processed.
This is useful if you want a specific variant of your handle_exc_class
to not
be handled by handle_errors
. For example, if you want to use
handle_exc_class=Exception
but you do not want handle_errors
to handle
RuntimeError
, then, you would set ignore_exc_class=RuntimeError
.
do_except
Often, it is useful to do some particular things when an exception is caught. Most
frequently this includes logging the exception. The do_except
optional argument
provides the ability to do this. The do_except
option should be a callable function
that accepts a parameter of type DoExceptParams
that can be imported from buzz
.
This dataclass
has three attributes:
err
: The caught exception itselfbase_message
: The message provided as the first argument tohandle_errors()
final_message
: A message describing the error (This will be the formatted error message)trace
: A stack trace
This option might be invoked something like this:
def log_error(dep: DoExceptParams):
logger.error(dep.final_message)
logger.error('\n'.join(dep.trace))
with handle_errors("Something went wrong", do_except=log_error):
some_dangerous_function()
do_else
This option describes some action that should happen if no exceptions are encountered.
This option is less useful than do_except
but it may useful in some circumstances.
This option should be a callable that takes no arguments:
def log_yay():
logger.info("we did it!")
with handle_errors("Something went wrong", do_else=log_yay):
some_not_dangerous_function()
do_finally
This option describes some action that should happen at the end of the context regardless to whether an exception occurred or not. This is a useful feature if you need to do some cleanup in either case. It should take a callable that receives no arguments:
def close_resource():
resource.close()
with handle_errors("Something went wrong", do_finally=close_resource):
some_dangerous_function_that_uses_resource(resource)
exc_builder
Functions the same as require_condition()
.
Expression checking context manager
The check_expressions()
context manager is used to check multiple expressions
inside of a context manager. Each expression is checked and each failing
expression is reported at the end in a raised exception. If no expressions fail
in the block, no exception is raised.
with check_expressions(main_message='there will be errors') as check:
check(True)
check(False)
check(1 == 2, "one is not two")
check('cooooooool', 'not a problem')
check(0, "zero is still zero")
If the above code was executed, an exception would be raised that looks like this:
Exception: Checked expressions failed: there will be errors
2: 2nd expression failed
3: one is not two
5: zero is still zero
The check_expressions()
context manager also accepts some keyword arguments:
raise_exc_class
Functions the same as require_condition()
.
raise_args
Functions the same as require_condition()
.
raise_kwargs
Functions the same as require_condition()
.
exc_builder
Functions the same as require_condition()
.
do_except
Functions the same as require_condition()
.
do_else
Functions the same as require_condition()
.
Additional Features
reformat_exception
This method is used internally by the handle_errors()
context manager. However, it is
sometimes useful in other circumstances. It simply allows you to wrap an exception
message in a more informative block of text:
try:
raise ValueError("I didn't like that")
except Exception as err:
print(reformat_exception("welp...that didn't work", err))
The above block would result in output like:
> welp...that didn't work -- ValueError: I didn't like that
get_traceback
This function is just a tool to fetch the traceback for the current function. It does
this by fetching it out of sys.exc_info
. It is used internally with Buzz but could
be useful in other contexts.
The Buzz base class
All of the methods described above are attached to the special exception class, Buzz
.
You could, for example, use this as the base exception type for your project and then
access all the functions of py-buzz
from that exception type:
from buzz import Buzz
class MyProjectError(Buzz):
pass
MyProjectError.require_condition(check_vals(), "Value check failed!")
The code above would raise a MyProjectError
with the supplied message if the condition
expression was falsey.
The Buzz
base class provides the same sort of access for:
enforce_defined()
ensure-type()
handle_errors()
check_expressions()
Demo
The py-buzz-demo
executable (only available when installed with the demo
extra) shows
the features described here in action. See the Demo page for more info.