tidyexc.Error

exception tidyexc.Error(brief='', **kwargs)[source]

A base class for making exceptions that:

  • Provide direct access to any parameters that are relevant to the error.

  • Use bullet-points to clearly and comprehensively describe the error.

Example:

>>> from tidyexc import Error
>>> class CheeseShopError(Error):
...     pass
...
>>> err = CheeseShopError(
...         product_name="Red Leicester",
...         num_requested=1,
...         num_available=0,
... )
>>> err.brief = "insufficient inventory to process request"
>>> err.info += "{num_requested} {product_name} requested"
>>> err.blame += "{num_available} available"
>>> raise err
Traceback (most recent call last):
  ...
tidyexc.exc.CheeseShopError: insufficient inventory to process request
• 1 Red Leicester requested
✖ 0 available

Public Data Attributes:

info_bullet

The prefix to use for each info message in the formatted error.

blame_bullet

The prefix to use for each blame message in the formatted error.

hint_bullet

The prefix to use for each hints message in the formatted error.

brief

A message template briefly describing the error.

brief_str

The brief message, with all parameter substitution performed.

info

Message templates describing the context in which the error occurred.

info_strs

The info messages, with all parameter substitution performed.

blame

Message templates describing the specific cause of the error.

blame_strs

The blame messages, with all parameter substitution performed.

hints

Message templates suggesting how to fix the error.

hint_strs

The hints messages, with all parameter substitution performed.

data

Parameters relevant to the error.

nested_data

A view providing access to all values defined for each parameter.

Inherited from BaseException

args

Public Methods:

add_info(*messages, **kwargs)

Add the given info to any exceptions derived from this class that are instantiated inside a with-block.

push_info(*messages, **kwargs)

Add the given info to any exceptions derived from this class that are subsequently instantiated.

pop_info()

Stop adding the info that was most-recently "pushed" to exceptions derived from this class.

clear_info()

Stop adding any info that was "pushed" to exceptions derived from this class.

put_info(*messages, **kwargs)

__init__([brief])

Create a new exception.

__str__()

Return the formatted error message.

Inherited from Exception

__init__(*args, **kwargs)

Inherited from BaseException

__repr__()

Return repr(self).

__str__()

Return str(self).

__getattribute__(name, /)

Return getattr(self, name).

__setattr__(name, value, /)

Implement setattr(self, name, value).

__delattr__(name, /)

Implement delattr(self, name).

__init__(*args, **kwargs)

__reduce__

helper for pickle

__setstate__

with_traceback

Exception.with_traceback(tb) -- set self.__traceback__ to tb and return self.


__init__(brief='', **kwargs)[source]

Create a new exception.

You can specify a brief message template and any number of parameters (via keyword arguments) when constructing an Error instance. This may be enough for simple exceptions, but most of the time you would subsequently add info and blame message templates (which cannot be directly specified in the constructor).

Any class-wide parameters and/or message templates specified using add_info() or push_info() are read and applied during the constructor. This means that these class-wide functions have no effect on exceptions that have already been instantiated.

Example:

>>> raise Error("a: {a}", a=1)
Traceback (most recent call last):
  ...
tidyexc.exc.Error: a: 1
__str__()[source]

Return the formatted error message.

All parameter substitutions are performed at this point, so any changes made to either the message templates or the data themselves will be reflected in the resulting error message.

classmethod add_info(*messages, **kwargs)[source]

Add the given info to any exceptions derived from this class that are instantiated inside a with-block.

A simple example:

>>> from tidyexc import Error
>>> with Error.add_info('a: {a}', a=1):
...     raise Error()
...
Traceback (most recent call last):
  ...
tidyexc.exc.Error:
• a: 1

The purpose of this function is to make it easier to add contextual information to exceptions. This is easiest to illustrate with an example. The following function parses a list of (x, y) coordinates from a file. Each coordinate appears on its own line and must consist of exactly two whitespace-separated numbers. There are several different errors this function should detect, but all of the error messages should include the file path, and most should also include the offending line number:

from tidyexc import Error

class ParseError(Error):
    pass

def parse_xy_coords(path):
    with ParseError.add_info("path: {path}", path=path):
        lines = _lines_from_file(path)
        coords = []

        for i, line in enumerate(lines, 1):
            with ParseError.add_info("line #{i}: {line}", i=i, line=line):
                coord = _coord_from_line(line)
                coords.append(coord)

        return coords

def _lines_from_file(path):
    try:
        with open(path) as f:
            return f.readlines()

    except FileNotFoundError:
        err = ParseError("can't read file")
        err.hints += "Double-check that the given path actually exists."
        raise err from None

def _coord_from_line(line):
    fields = line.split()

    if len(fields) != 2:
        raise ParseError(
                lambda e: f"expected 2 fields, found {len(e.fields)}",
                fields=fields,
        )

    coord = []

    for field in fields:
        try:
            coord.append(float(field))

        except ValueError:
            raise ParseError("expected a number, not {field!r}", field=field) from None

    return tuple(coord)

Using add_info() simplifies the above code in two major ways:

  • Each piece of contextual information is specified just once and used by multiple exceptions. There’s no way to accidentally raise an exception without this information.

  • The helper functions that actually raise the exceptions don’t need to have access to any of the contextual information. Without add_info(), it would either be necessary to pass extra arguments around or to catch and re-raise the exceptions.

It is possible to nest any number of these context managers. The info bullet points will appear in the order the context managers were invoked. It is considered good practice for the message templates to only make use of the kwargs parameters provided to the same context manager, but templates can access all previously defined parameters. They cannot access any subsequently defined parameters. If a parameter is defined multiple times, the most recent previous value will be used. For example:

>>> # This template cannot use the `c` parameter, because it has
>>> # not been defined yet.  Note that the `b` parameter keeps its
>>> # value, even though it is subsequently redefined.
>>> with Error.add_info('a={a} b={b}', a=1, b=2):
...
...     # The `a` parameter can be used in this template because
...     # it was defined previously.  The `b` parameter shadows the
...     # previous value.
...     raise Error('a={a} b={b} c={c}', b=3, c=4)
...
Traceback (most recent call last):
  ...
tidyexc.exc.Error: a=1 b=3 c=4
• a=1 b=2

Because templates cannot be affected by subsequent parameters, it is safe to use add_info() in recursive functions, where the same exact parameters might be specified multiple times with different values. The data attribute provides access to the current value of each parameter. The nested_data attribute, in contrast, provides access to all values for each parameter.

property blame

Message templates describing the specific cause of the error.

For example, imagine an error that was triggered because an unmatched brace was encountered when parsing a file. A good blame message for this exception would clearly state that an unmatched brace was the cause of the problem.

See info for a description of message templates in general.

Example:

>>> from tidyexc import Error
>>> err = Error(a=1)
>>> err.blame += "a: {a}"
>>> raise err
Traceback (most recent call last):
  ...
tidyexc.exc.Error:
✖ a: 1
blame_bullet = '✖ '

The prefix to use for each blame message in the formatted error.

property blame_strs

The blame messages, with all parameter substitution performed.

property brief

A message template briefly describing the error.

The tidyverse style guide recommends using the verb “must” when the cause of the problem is pretty clear, and the verb “can’t” when it’s not. It’s common for this template to be a fixed string (i.e. no parameter substitution), and for the info and blame templates to reference the parameters of the exception.

See info for a description of message templates in general. Unlike info, blame, and hints, there can only be a single brief message template. For this reason, use the assignment operator the set this template (as opposed to the in-place addition operator).

Example:

>>> err = Error(a=1)
>>> err.brief = "a: {a}"
>>> raise err
Traceback (most recent call last):
  ...
tidyexc.exc.Error: a: 1
property brief_str

The brief message, with all parameter substitution performed.

classmethod clear_info()[source]

Stop adding any info that was “pushed” to exceptions derived from this class.

property data

Parameters relevant to the error.

This attribute is a dictionary-like object that allows parameters to be accessed either as attributes or as dictionary elements:

>>> e = Error(a=1)
>>> e.data.a
1
>>> e.data['a']
1

If a parameter has been defined multiple times (e.g. with add_info()), the most recent value is the one that will be used:

>>> with Error.add_info(a=1):
...     e = Error(a=2)
...     e.data.a
2
hint_bullet = '• '

The prefix to use for each hints message in the formatted error.

property hint_strs

The hints messages, with all parameter substitution performed.

property hints

Message templates suggesting how to fix the error.

For example, imagine an error that was triggered because some user input didn’t match any of the expected keywords. A good hint for this exception might suggest the expected keyword that was most similar to what the user inputted.

See info for a description of message templates in general.

Example:

>>> from tidyexc import Error
>>> err = Error(a=1)
>>> err.hints += "a: {a}"
>>> raise err
Traceback (most recent call last):
  ...
tidyexc.exc.Error:
• a: 1
property info

Message templates describing the context in which the error occurred.

For example, imagine an error that was triggered because an unmatched brace was encountered when parsing a file. A good info message for this exception might specify the file name and line number where the error occurred.

A message template can either be a string or a callable:

  • str: When the error message is generated, the str.format method will be called on the string as follows: s.format(**self.data). This means that any parameters associated with the exception can be substituted into the message.

  • callable: When the error message is generated, the callable will be invoked with data as the only argument. It should return a string or a list of strings, which will be taken as the message(s). A common use-case for callable template is to specify f-strings via lambda functions. This is a succinct way to format parameters using arbitrary expressions (see example below).

The info, blame, and hints attributes are all list-like objects containing message templates. Special syntax is added such that you can use the += operator to add message templates to any of these lists. You can also use any of the usual list methods to modify the list in-place, although you cannot overwrite these attributes altogether.

The info attribute alone has an additional method called layers() that returns each info template paired with the index that can be passed to nested_data.flatten() to get the parameters associated with that particular template.

Example:

>>> err = Error(a=1, b=[2,3])
>>> err.info += "a: {a}"
>>> err.info += lambda e: f"b: {','.join(map(str, e.b))}"
>>> raise err
Traceback (most recent call last):
  ...
tidyexc.exc.Error:
• a: 1
• b: 2,3
info_bullet = '• '

The prefix to use for each info message in the formatted error.

property info_strs

The info messages, with all parameter substitution performed.

property nested_data

A view providing access to all values defined for each parameter.

This is useful when trying to extract information from an exception where some parameters may have been defined multiple times, e.g. if add_info() was used in a recursive function.

The simplest way to use this view is to access a parameter name either as an attribute or a dictionary element. This will return a list of all the values associated with that parameter:

>>> with Error.add_info(a=1):
...     e = Error(a=2)
...     e.nested_data.a
[1, 2]

The [] operator will also accept a tuple of parameter names, in which case it will return a list of those parameters in every context in which at least one of those parameters was defined. This is useful if you’re interested in parameters that are logically connected (e.g. line and column number) and you want to avoid the possibility of them getting out of sync:

>>> with Error.add_info(a=1, b=2):
...     with Error.add_info(a=3, b=4):
...         e = Error(c=5)
...         e.nested_data['a','b']
[(1, 2), (3, 4)]

Finally, the view also has a flatten() method that can be used to get all the values for each parameter defined at a particular point in time. The method accepts a single argument which will be used as an index into the internal list of contexts. The data.layers() method can be used to get the index corresponding to any info message template.

classmethod pop_info()[source]

Stop adding the info that was most-recently “pushed” to exceptions derived from this class.

This is the opposite of push_info().

classmethod push_info(*messages, **kwargs)[source]

Add the given info to any exceptions derived from this class that are subsequently instantiated.

See add_info() for more information. This function is similar, except that it is not a context manager. That means that you must manually call pop_info() or clear_info() after calling this function.

Example:

>>> from tidyexc import Error
>>> Error.push_info('a: {a}', a=1)
>>> try:
...     raise Error()
... finally:
...     Error.pop_info()
...
Traceback (most recent call last):
  ...
tidyexc.exc.Error:
• a: 1