Skip to content

Commit f871e77

Browse files
joelostblomlwjohnst86martonvago
authored
feat: ✨ explain() errors (#208)
Creates a more human readable format to explain error messages Closes #11, closes #58, closes #14 Needs an in-depth review. ## Checklist - [x] Ran `just run-all` --------- Co-authored-by: Luke W. Johnston <lwjohnst86@users.noreply.github.com> Co-authored-by: martonvago <57952344+martonvago@users.noreply.github.com>
1 parent ac2adce commit f871e77

File tree

5 files changed

+58
-17
lines changed

5 files changed

+58
-17
lines changed

docs/design/interface.qmd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ decide when and how failed checks should be enforced or handled. The
7676
output of this function is a list of `Issue` objects, which are
7777
described below.
7878

79-
### {{< var planned >}} `explain()`
79+
### {{< var done >}} `explain()`
8080

8181
The output of `check()` is a list of `Issue` objects, which are
8282
structured and machine-readable, but not very human-readable and

docs/guide/issues.qmd

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ properties. An `Issue` has attributes that provide information about the
1212
failed check, which are described in more detail in the
1313
[`Issue`](/docs/reference/Issue.qmd) documentation.
1414

15-
<!-- TODO: Unhide this after it has been implemented. -->
16-
17-
::: content-hidden
1815
## The `explain()` function
1916

2017
The `explain()` function provides a more verbose and user-friendly
@@ -40,4 +37,3 @@ cdp.explain(issues)
4037

4138
When setting `error=True` in `check()`, error messages will be generated
4239
by the `explain()` function.
43-
:::

src/check_datapackage/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Check functions and constants for the Frictionless Data Package standard."""
22

3-
from .check import DataPackageError, check
3+
from .check import DataPackageError, check, explain
44
from .config import Config
55
from .examples import (
66
example_field_properties,
@@ -24,5 +24,6 @@
2424
"example_resource_properties",
2525
"example_field_properties",
2626
"check",
27+
"explain",
2728
"read_json",
2829
]

src/check_datapackage/check.py

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,56 @@ def __init__(
4848
self,
4949
issues: list[Issue],
5050
) -> None:
51-
"""Create the DataPackageError attributes from issues."""
52-
# TODO: Switch to using `explain()` once implemented
53-
errors: list[str] = _map(
54-
issues,
55-
lambda issue: f"- Property `{issue.jsonpath}`: {issue.message}\n",
56-
)
57-
message: str = (
58-
"There were some issues found in your `datapackage.json`:\n\n"
59-
+ "\n".join(errors)
51+
"""Create the DataPackageError from issues."""
52+
super().__init__(explain(issues))
53+
54+
55+
def explain(issues: list[Issue]) -> str:
56+
"""Explain the issues in a human-readable format.
57+
58+
Args:
59+
issues: A list of `Issue` objects to explain.
60+
61+
Returns:
62+
A human-readable explanation of the issues.
63+
64+
Examples:
65+
```{python}
66+
import check_datapackage as cdp
67+
68+
issue = cdp.Issue(
69+
jsonpath="$.resources[2].title",
70+
type="required",
71+
message="The `title` field is required but missing at the given JSON path.",
6072
)
61-
super().__init__(message)
73+
74+
cdp.explain([issue])
75+
```
76+
"""
77+
issue_explanations: list[str] = _map(
78+
issues,
79+
_create_explanation,
80+
)
81+
num_issues = len(issue_explanations)
82+
singular_or_plural = " was" if num_issues == 1 else "s were"
83+
return (
84+
f"{num_issues} issue{singular_or_plural} found in your `datapackage.json`:\n\n"
85+
+ "\n".join(issue_explanations)
86+
)
87+
88+
89+
def _create_explanation(issue: Issue) -> str:
90+
"""Create an informative explanation of what went wrong in each issue."""
91+
# Remove suffix '$' to account for root path when `[]` is passed to `check()`
92+
property_name = issue.jsonpath.removesuffix("$").split(".")[-1]
93+
number_of_carets = len(str(issue.instance))
94+
return ( # noqa: F401
95+
f"At package{issue.jsonpath.removeprefix('$')}:\n"
96+
"|\n"
97+
f"| {property_name}{': ' if property_name else ' '}{issue.instance}\n"
98+
f"| {' ' * len(property_name)} {'^' * number_of_carets}\n"
99+
f"{issue.message}\n"
100+
)
62101

63102

64103
def check(
@@ -579,6 +618,7 @@ def _create_issue(error: SchemaError) -> Issue:
579618
message=error.message,
580619
jsonpath=error.jsonpath,
581620
type=error.type,
621+
instance=error.instance,
582622
)
583623

584624

src/check_datapackage/issue.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from dataclasses import dataclass
1+
from dataclasses import dataclass, field
2+
from typing import Any
23

34

45
@dataclass(order=True, frozen=True)
@@ -15,6 +16,8 @@ class Issue:
1516
as "required", "type", "pattern", or "format", or a custom type). Used to
1617
exclude specific types of issues.
1718
message (string): A description of what exactly the issue is.
19+
instance (Any): The part of the object that failed the check. This field is not
20+
considered when comparing or hashing `Issue` objects.
1821
1922
Examples:
2023
```{python}
@@ -31,3 +34,4 @@ class Issue:
3134
jsonpath: str
3235
type: str
3336
message: str
37+
instance: Any = field(default=None, compare=False, hash=False)

0 commit comments

Comments
 (0)