Error Reporting

Not every fit goes smoothly and especially when you’re building your model you will get misfits. If the system just told you blindly “there’s an error” it would not be very convenient, that’s why typefit was built around the idea of providing great error feedback.

Note

This feature is still experimental because it needs to be in the wild for a while before making any definitive decisions.

Default behavior

By default, errors will be reported using the logging module and will be output with True Colors ANSI codes.

This is the output of the default fitter:

Fitter(
    error_reporter=LogErrorReporter(
        formatter=PrettyJson5Formatter(colors='terminal16m')
    )
)

This means that if you call typefit() directly and a parsing error occurs, right before the ValueError exception is raised, the logger will receive a message explaining the error.

Rationale

The main goal of error reporting is that humans are able to understand the error.

Errors might occur in simple structures but they are more likely to happen in complicated recursive structures.

Suppose that you have a list of a thousand items and hidden in that list there is just one or two items that don’t match. Or the opposite, all items have an error. Or even worse, you have a recursive structure with unions which generate an exponentially high number of matching errors.

How do you display these errors without flooding the user’s console?

The main idea behind the error reporting in typefit is that errors should be displayed on top of the data, this way you can’t have more error than fields in your data.

On top of that, errors are abbreviated as much as possible. Objects that don’t contain errors are not expanded. Lists that have a thousand times the same error will only display the error once. Strings that are too long will get truncated.

Those constraints lead to develop the PrettyJson5Formatter formatter, which will generate colored and annotated output, easy to grasp when looking for an error.

Note

For now, everything is very oriented towards a developer that sees some output in a terminal to help them debug things. This explains the very opinionated nature of default configuration. Feel free to open tickets to make default more sane for other configurations!

Custom behavior

Maybe that you are not happy with the default behavior and you wish to change it with custom reporting/formatting. This is possible through the fact that you can configure what you want when creating the Fitter instance.

Reporter

By creating an implementation of ErrorReporter that you then pass to the constructor of Fitter, you can then get notified of every error and report it.

The default reporter reports to the logging however feel free to report to whichever tool you want, be it a live front-end or an IDE plugin.

If your reporter reports text, you might be interested in using the standard formatter interface, as explained right below.

Formatter

While you don’t have to use the formatter when doing a reporter, if the reporter is going to report text then you can use this interface.

That formatter will take in the node that has an error and will output a legible text that can be understood by the user.

Formatters must implement the ErrorFormatter interface.

The default formatter, PrettyJson5Formatter, is very opinionated and will generate shortened JSON5 outputs that are easy to read and potentially colored using Pygments (AINSI codes, HTML, IRC, …).

Reference

For more information about the implementations, you can have a look at the reference documentation.

Reporting

The reporting module has all the reporting logic.

class typefit.reporting.ErrorFormatter

This interface is in charge of converting the meta information from Node into some readable text report. That’s useful for some reporters, like the LogErrorReporter.

abstract format(node: typefit.nodes.Node)str

Implement this in order to transform the node into some readable text.

Parameters

node – Node that failed to be parsed, with errors

class typefit.reporting.ErrorReporter

Implement this interface to be able to report errors. Once configured into the Fitter, every time a fitting fails this will be triggered with the node that failed to be converted so that the error can be reported to the developer for debugging purposes.

You can imagine reporters that send the error to the logging module (see LogErrorReporter) but it could as well be displaying the data in an interactive web page or an IDE plugin to help seeing clearly what is happening.

abstract report(node: typefit.nodes.Node)None

Will be called by Fitter for every error. Override it in order to report the error wherever you want to report it to.

Parameters

node – Node that failed to fit

class typefit.reporting.LogErrorReporter(formatter: typefit.reporting.ErrorFormatter, level: int = 40)

Reports the errors into the logging module

Parameters
  • formatter – Formatter to use

  • level – Log level to report the errors with

report(node: typefit.nodes.Node)None

Will be called by Fitter for every error. Override it in order to report the error wherever you want to report it to.

Parameters

node – Node that failed to fit

class typefit.reporting.PrettyJson5Formatter(indent: str = '    ', colors: str = '', truncate_strings_at: int = 40)

Formats the data into annotated and redacted JSON5. The goal here is to display the errors on top of the actual data, while removing all the data that is not useful to understand the error itself.

The output can be colored by Pygments, just fill in the colors argument with a valid Pygments formatter name (could be ANSI codes, could be HTML, could be whatever you desire).

Parameters
  • indent – Content of one ident level (default = 4 spaces)

  • colors – Pygments color formatter. No coloring happens if this string is empty.

  • truncate_strings_at – If a string is longer than this indicated length then it will be truncated for display. Default is 40, put 0 to disable the feature.

format(node: typefit.nodes.Node)str

Formats the node into JSON5. If colors were specified in the constructor, then it’s also where the coloration is added before being returned.

Nodes

The full output of a parsing, including error messages, is held by the Node models. If you are making a custom formatter or reporter, you might have to work with this API.

class typefit.nodes.FlatNode(fitter: Fitter, value: Any)

Flat nodes are mostly the builtin types (int, float, str, bool, None) but also can be class constructors if the constructor can accept what is being passed to it as an argument.

fit(t: Type[typefit.nodes.T])typefit.nodes.T

Tries to use the builtins to fit and if not tries to see if there is a constructor we can use.

Parameters

t – Type you’re fitting into

fit_builtin(t: Type[typefit.nodes.T])Optional[typefit.nodes.T]

For builtins, we dumbly test one by one. Please note that there is a trick where an int can implicitly be converted into a float (since it’s coming from JSON we know that the int precision is already the one of a float so we don’t risk on loosing precision).

Returns None when trying to convert into a type that is not a builtin.

Parameters

t – Type to convert into, if not a builtin the None will be returned

fit_class(t: Type[typefit.nodes.T])typefit.nodes.T

Fitting the content into a class. That one is a bit tricky but basically if the constructor can accept one argument and that this argument is properly annotated and that the value can fit into that argument’s type then the constructor will be called with the fitted argument.

However, so far it only works with builtin types. Arbitrary types might come in later, but this is mostly here so that you can transform strings into dates.

class typefit.nodes.ListNode(fitter: Fitter, value: Any, children: List[typefit.nodes.Node], problem_is_kids: bool = False)

Node that is parent to a list of other nodes. That’s basically the application of the same type on a list of items instead of just one.

Parameters
  • children (List[typefit.nodes.Node]) – List of child nodes

  • problem_is_kids (bool) – Does the rejection come from the kids? Or is it due to this node only?

fit(t: Type[typefit.nodes.T])typefit.nodes.T

After some sanity checks, goes through the whole list to make sure that all the content fits. If some of them don’t fit, still continue to try fitting the rest, for error reporting purposes.

class typefit.nodes.MappingNode(fitter: Fitter, value: Any, children: Dict[str, typefit.nodes.Node], missing_keys: List[str] = <factory>, unwanted_keys: List[str] = <factory>, problem_is_kids: bool = False)

Those nodes are triggered by JSON “objects”. They can map into different kinds of Python objects:

  • Plain old dictionaries (with string keys)

  • Dataclasses

  • Type-annotated named tuples

Parameters
  • children (Dict[str, typefit.nodes.Node]) – Dictionary of child nodes

  • missing_keys (List[str]) – After fitting, which keys were missing (warning, so far this is tricky for unions)

  • unwanted_keys (List[str]) – After fitting, which keys were unwanted (warning, so far this is tricky for unions)

  • problem_is_kids (bool) – Does the rejection come from the kids? Or is it due to this node only?

fit(t: Type[typefit.nodes.T])typefit.nodes.T

This detects if we’re dealing with a fit into a dictionary or a fit into a named tuple/dataclass.

Parameters

t – Type to fit into, either a Dict either NamedTuple either a @dataclass

fit_dict(t: Type[typefit.nodes.T])typefit.nodes.T

Fitting a JSON object into a Python dictionary. That’s basically just a copy with a few sanity checks (and typefitting on the values).

Notes

If we’re fitting to a dictionary we’re first going to make sure that the type spec makes sense and in particular that the keys are strings. Indeed, having keys being something else than strings would be much more complicated due to the mappings and other transformations happening. On the other side, we’re doing this for JSON and there is no other possibility for keys.

On the other hand, the values can be fitted into any type that the developer asks and a lot of the code is just doing this fitting and reporting errors.

Parameters

t – Should be a dictionary specification, otherwise it’s going to fail

fit_object(t: Type[typefit.nodes.T])typefit.nodes.T

Fitting into dataclasses and named tuples. On paper that’s fairly simple, but if you want something safe with good error reporting it’s a little bit more complicated.

Notes

The first thing that happens is different kinds of inspection on the type to figure what are the members of that object, what are their types, which ones are mandatory and which ones have a default value, etc.

Then we’ll look at each key into the source data and if the data is there it’ll be fitted into the expected type.

Please note that there is also some source mapping happening, if the dataclass metadata contains a source we’re going to use the source function instead of directly digging into the value.

Subsequently, with a few set operations, you can deduce which fields are missing, which fields are too much, etc. If anything is wrong, we report and fail.

Finally, the object is instantiated. If the dataclass/named tuple has not been tampered with too much, this should pass since we’ve checked that the parameters are right. If there is some funny business going on then we’re not going to be able to get a decent explanation to the user, even potentially crash the whole thing with no explanation. I have no better idea on how to handle this though.

Parameters

t – Type-annotated named tuple class or dataclass

class typefit.nodes.Node(fitter: Fitter, value: Any)

Typefit is made to convert JSON structure into Python dataclasses and objects. In order to make analyzing a fitting possible after it failed, data structures will be converted into a tree of Nodes. This is the base class, but there is 3 kinds of nodes:

  • FlatNode – “Flat” structures, aka numbers, strings, bools or nulls

  • MappingNode – For mappings (dictionaries, dataclasses and named tuples)

  • ListNode – For lists

Each node will remember the errors that the decoder went through and if the decoding was successful at all. In other terms, by navigating through the nodes you can generate readable error reporting.

Parameters
  • fitter (Fitter) – Fitter instance to refer to for configuration and fitting other nodes

  • value (Any) – Value of this node

  • errors (Set[str]) – All unique errors encountered on this node. There might still be errors if the fitting was successful, as those errors record all attempts.

  • fit_success (bool) – Indicates if the fitting was successful at least once. This does not mean that the overall fitting went well and especially with unions this could be tricky.

fail(reason: str)None

Utility to trigger a failure

  • Register the error locally.

  • Raise a value error. Nodes that have children nodes need to catch the value errors from their children in order to not stop their own processing.

Parameters

reason – Reason to be given and registered as error

fit(t: Type[typefit.nodes.T])typefit.nodes.T

That is left to the subclasses to implement