Package: kwexception
kwexception: Better exceptions with keyword parametersMotivationMost Python exceptions consist of an error type (ValueError,TypeError,
etc.) and a message that attempts to communicate the problem. In many
situations, that message must contain one or more data values to provide
context. In simple cases, exceptions created in the classic style are not too
bad.raiseValueError(f'Cannot convert value to float:{val!r}')But when the data needed to create a clear exception message expands to to
multiple or more complex values, the process becomes both tedious and
ill-conceived. Tedious because the programmer must engage in ad
hoc string-formatting maneuvers. Ill-conceived because something explicit and
useful to programmers (data) is wedged into a string of text, making the data
less immediately accessible (for example, quickly copying it into an editor or
REPL) and less explicit (sometimes important details are lost in
stringification). Here's a lightly-edited example taken from a high quality,
widely used Python library illustrating how the typical approach to exceptions
can quickly lead to awkward string-building gynastics.raiseTypeError("'{name}' must be{type!r}(got{value!r}that is a ""{actual!r}).".format(name=name,type=self.type,actual=value.__class__,value=value,),)Similar problems have long existed in more pressing forms in the domain of
logging. The classic approach was to emit logging messages in the manner
described above: take a human-readable message format and then insert data
values into it. The end result is a logging message that is only
partially-parsable unless the developers on the project exercise unusually high
levels of discipline in their creation of logging messages. Seeking a better
alternative, many software engineers have switched to JSON-based logging. Under
that approach, the human-readable text is just a short message stating the
problem in general terms, and that message is just one key-value pair in a dict
that contains all other data parameters needed to make the logging entry
specific and meaningful.Python exceptions are amendable to similar improvements – hence the
kwexception library. Instead of starting with some text and shoving data into
it, the developer creates an exception via keyword parameters.Basic usageThe first step is to define one or more exception classes for your project. If
you are satisfied with the library's default behavior, those classes just need
to inherit fromKwexception.fromkwexceptionimportKwexceptionclassPointError(Kwexception):passTo create exceptions, pass both the textual message and any other keyword
parameters needed to make the error useful. By default, the message is stored
under themsgkeyword parameter. When creating exceptions, you can pass the
message explicitly under that key or as the first positional parameter. When
writing the text, avoid the temptation to put data values inside it: the
philosphy of the library is to keep the textual, but general, statement of the
problem separate from the specific data values relevant to the error at hand.INVALID='Invalid Point coordinates'x=11y=Nonee=PointError(msg=INVALID,x=x,y=y)# Pass msg explicitly.e=PointError(INVALID,x=x,y=y)# Or as the first positional.The exception's data will be accessible via itsparamsattribute.print(e.params)# {'msg': 'Invalid Point coordinates', 'x': 11, 'y': None}Instances of the class have amsgproperty to retrieve the value at
themsgkey inparams, in the fashion ofdict.get().print(e.msg)# Invalid Point coordinatesWhen the exception is stringified, its data will be presented faithfully as a
dict.# str() representation.{'msg':'Invalid Point coordinates','x':11,'y':None}# repr() representation.PointError({'msg':'Invalid Point coordinates','x':11,'y':None})# Stacktrace representation.PointError:{'msg':'Invalid Point coordinates','x':11,'y':None}Upon first exposure to such output one might balk at the aesthetics of the dict
when compared to a classic exception with just a human-readable message. But
stacktraces – and exception stringification generally – are the domain of
software engineers, not end users, so those aesthetics concerns are misplaced
(if your end-users are seeing your stacktraces, your project has bigger
problems). For Python programmers, there is nothing mysterious or unsightly
about a dict; they are eminently clear and beautifully practical.Setting a default message, or several of themIn many situations, it makes sense to use one message for each exception type.
In that case, theKwexceptionsubclass can declare aDEFAULT_MSG, further
simplifying the process of creating the exception.classPointError(Kwexception):DEFAULT_MSG='Invalid Point coordinates'e=PointError(x=11,y=None)print(e.params)# {'msg': 'Invalid Point coordinates', 'x': 11, 'y': None}Alternatively, the user can define multiple defaults, either in the form of a
mapping or object. During object creation, the suppliedmsgvalue will be
used as a key or attribute to retrieve the desired default.classPointError(Kwexception):MSGS=dict(coord='Invalid Point coordinates',neg='Negative coordinates currently disallowed',)e=PointError(PointError.MSGS['coord'],x=11,y=None)Old-school data-bearing messagesPerhaps you like the central idea of the kwexception library – maintaining a
separation between the textual message and the data values – but either you
are a traditionalist at heart or your project still requires data-bearing,
human-readable messages for some other purpose (for example, a situation where
you do need to assemble a user-facing message, not a stacktrace, and an
exception's data provides the most logical mechanism to do that).The kwexception library supports that use case via theFORMAT_MSGattribute.
If true, during object creation theKwexceptionsubclass will treat the
initially-derivedmsgnot not as a literal message but as a Python
format-string. The ultimatemsgis then derived via astr.format()call,
passing the exception's keyword parameters as arguments to that call.Optionally,FORMAT_MSGcan be combined with eitherDEFAULT_MSGorMSGS,
as illustrated here.classPointError(Kwexception):DEFAULT_MSG='Invalid Point coordinates: x={x}y={y}'FORMAT_MSG=Truee=PointError(x=11,y=None)print(e.params)# {'msg': 'Invalid Point coordinates: x=11 y=None', 'x': 11, 'y': None}Details on the exception data model on stringificationThe underlying data model for aPython exceptionis a
tuple, accessible via theargsattribute.ve1=ValueError('Boom')ve1.args# ('Boom',)ve2=ValueError('Boom',1,2)ve2.args# ('Boom', 1, 2)AKwexceptionsubclass rests on that behavior, with the dict of keyword
parameters typically being the sole element in theargstuple. For example,
the PointError shown above would have the following tuple.({'msg':'Invalid Point coordinates','x':11,'y':None},)When a Python exception'sargstuple has just one element (which is the
situation in the overwhelming majority of cases), stringification takes a
simplified form. One can see this by comparing the two ValueError instances
shown above.print(str(ve1))# Boomprint(str(ve2))# ('Boom', 1, 2)TheKwexceptionlibrary provides an analogous simplification when its instances
are stringified. If the instance has only amsgin its keyword parameters and
if itsargstuple consists of nothing but the dict of those parameters, the
exception will be displayed in simple form.e1=PointError('Foo',x=11,y=None)e2=PointError('Foo')e3=PointError(msg='Foo')print(str(e1))# {'msg': 'Foo', 'x': 11, 'y': None}print(repr(e1))# PointError({'msg': 'Foo', 'x': 11, 'y': None})print(str(e2))# Fooprint(repr(e2))# PointError('Foo')print(str(e3))# Fooprint(repr(e3))# PointError('Foo')Exception handling and augmentationTheKwexceptionclass provides another primary feature: the ability to handle
other exceptions in an easier, more consistent way. This behavior is provided
via the class methodnew(), which takes an exception as its first argument
and optionally takes any other keyword parameters. Its intended usage is in atry-exceptcontext.try:...exceptExceptionase:# The original error might or might not be a PointError.# Our application wants to ensure that it is.e=PointError.new(e,msg='foo',x=x,y=y)...If the exception provided toKwexception.new()is already an instance of the
relevantKwexceptionsubclass, the method returns the same exception
instance, but updates itsparamsdict with the keyword parameters supplied tonew().e1=PointError('foo',a=1,b=2)e2=PointError.new(e1,a=111,c=3)print(e2ise1)# Trueprint(repr(e2))# PointError({'msg': 'foo', 'a': 111, 'b': 2, 'c': 3})If the provided exception is some other type of error, thenew()method
returns a newKwexceptionsubclass instance with the provided keyword
parameters, plus additional parameters providing contextual information about
the original exception's type andargs.ve1=ValueError('foo',99)e3=PointError.new(ve1,msg='bar',x=1)print(repr(e3))# PointError({'msg': 'bar', 'x': 1,# 'context_error': 'ValueError', 'context_args': ('foo', 99)})CustomizationAKwexceptionsuperclass offers a few customizations for users who want some,
but not all, of its default behaviors. This example lists the default settings.classPointError(Kwexception):# Key name for the Kwexception message in self.params.MSG_KEY='msg'# Whether and how to set msg from the first positional.MOVE='move'COPY='copy'SET_MSG=Kwexception.MOVE# Kwexception.MOVE, Kwexception.COPY, or None.# Default msg for instances of the class.DEFAULT_MSG=None# Default msg values for instances of the class. Accepts mapping or object.MSGS=None# Whether to use the initially-derived msg value as format string.FORMAT_MSG=False# Whether to add params to args.ADD_PARAMS_TO_ARGS=True# Whether to simplify stringification for message-only exceptions.SIMPLIFY_DISPLAY=True# Whether to treat a single positional dict as the keyword params.SINGLE_DICT_AS_PARAMS=True# Whether new() should use update or setdefault when augmenting params.NEW_UPDATE=True# Whether new() should convert errors of another type to the relevant# Kwexception subclass and, if so, whether to include contexutal# information in params.NEW_CONVERT=TrueNEW_CONTEXT=True# Key names for contextual information provided by new().CONTEXT_ERROR='context_error'CONTEXT_ARGS='context_args'Controlling the key name for the exception message:MSG_KEY. TheKwexceptioninstance's message is stored under themsgkey. To use a
different naming convention, setMSG_KEYto a different value and define an
alias for theKwexception.msg()property. Here is an illustration for those
preferring a more verbose but explicit approach.classPointError(Kwexception):MSG_KEY='message'message=Kwexception.msgSetting the message from the first positional:SET_MSG. By default, the
first positional argument is treated as themsgand is moved out of the tuple
of positionals and into the dict of keyword parameters. Alternatively, that
move operation can be a copy operation, or disabled entirely.Defining default message(s):DEFAULT_MSGandMSGS. Specify either a
defaultmsgvalue or a dict or object holding such values, as discussed
above.Data-bearing messages:FORMAT_MSG. If true treat the initially-derivedmsgas a Python format-string, as discussed above.Adding the dict of keyword parameters to theargstuple:ADD_PARAMS_TO_ARGS. By default, the dict of keyword parameters is appended
to the exception'sargstuple (this occurs after the move/copy forSET_MSG).
If aKwexceptionsubclass wants to take advantage of keyword parameters but
also needs theargstuple for other purposes, this behavior can be disabled.Simplified display for message-only exceptions:SIMPLIFY_DISPLAY. As
documented above, by default aKwexceptioninstance containing no data other
than amsgwill stringify in a simplified way. If the behavior is disabled,
stringification will be based on the content ofargsusing default Python
behavior.Accept keyword parameters via a positional dict:SINGLE_DICT_AS_PARAMS.
By default, aKwexceptioninstance is stringified forrepr()by showing the
dict of keyword parameters. For consistency with that representation, if the
constructor is given only a dict positionally (i.e., no other positional or
keyword arguments), it will treat that dict as the exception's keyword
parameters and store them inparamsaccordingly.Kwexception.new(): augment keyword parameters via update or setdefault:NEW_UPDATE. When given an instance of the relevantKwexceptionsubclass,
the classmethodnew()uses the keyword parameters to augment the original
exception'sparamsdict in the manner ofdict.update(). IfNEW_UPDATEis
set to false, theparamsdict is augmented in the manner ofdict.setdefault.Kwexception.new(): whether to convert exceptions and add contextual
information about the original:NEW_CONVERTandNEW_CONTEXT. When given
an instance of a non-Kwexceptiontype, the classmethodnew()returns a new
exception of the relevantKwexceptionsubclass and it includes contextual
information in theparamsdict about the original error. Alternatively, one
can suppress the inclusion of contextual information or the entire conversion
process.Kwexception.new(): key names for contextual information:CONTEXT_ERRORandCONTEXT_ARGS. Modify as needed.