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 theLogErrorReporter
.- 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 nullsMappingNode
– 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.
See also
- 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