PySerials 0.1.3__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyserials-0.1.3 → pyserials-0.2.0}/PKG-INFO +1 -1
- {pyserials-0.1.3 → pyserials-0.2.0}/pyproject.toml +1 -1
- {pyserials-0.1.3 → pyserials-0.2.0}/setup.py +1 -2
- {pyserials-0.1.3 → pyserials-0.2.0}/src/PySerials.egg-info/PKG-INFO +1 -1
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/__init__.py +16 -3
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/compare.py +1 -1
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/exception/__init__.py +3 -1
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/exception/_base.py +5 -4
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/exception/read.py +35 -29
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/exception/update.py +17 -13
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/exception/validate.py +77 -28
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/flatten.py +6 -4
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/format.py +21 -12
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/nested_dict.py +13 -10
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/property_dict.py +5 -8
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/read.py +40 -26
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/update.py +385 -159
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/validate.py +30 -20
- {pyserials-0.1.3 → pyserials-0.2.0}/src/pyserials/write.py +22 -12
- {pyserials-0.1.3 → pyserials-0.2.0}/setup.cfg +0 -0
- {pyserials-0.1.3 → pyserials-0.2.0}/src/PySerials.egg-info/SOURCES.txt +0 -0
- {pyserials-0.1.3 → pyserials-0.2.0}/src/PySerials.egg-info/dependency_links.txt +0 -0
- {pyserials-0.1.3 → pyserials-0.2.0}/src/PySerials.egg-info/not-zip-safe +0 -0
- {pyserials-0.1.3 → pyserials-0.2.0}/src/PySerials.egg-info/requires.txt +0 -0
- {pyserials-0.1.3 → pyserials-0.2.0}/src/PySerials.egg-info/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""PySerials:
|
|
1
|
+
"""PySerials: read, write, validate, and transform serialized data formats.
|
|
2
2
|
|
|
3
3
|
PySerials provides utilities for working with JSON, YAML, and TOML data formats,
|
|
4
4
|
including reading/writing files and strings, recursive dictionary updates,
|
|
@@ -32,7 +32,20 @@ PropertyDict
|
|
|
32
32
|
A dictionary wrapper supporting attribute-style access.
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
__all__ = [
|
|
36
|
+
"NestedDict",
|
|
37
|
+
"PropertyDict",
|
|
38
|
+
"compare",
|
|
39
|
+
"exception",
|
|
40
|
+
"flatten",
|
|
41
|
+
"format",
|
|
42
|
+
"read",
|
|
43
|
+
"update",
|
|
44
|
+
"validate",
|
|
45
|
+
"write",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
from pyserials import compare, exception, format, read, update, validate, write
|
|
49
|
+
from pyserials.flatten import flatten
|
|
36
50
|
from pyserials.nested_dict import NestedDict
|
|
37
51
|
from pyserials.property_dict import PropertyDict
|
|
38
|
-
from pyserials.flatten import flatten
|
|
@@ -72,7 +72,7 @@ def items(
|
|
|
72
72
|
recursive_compare(src[i], trg[i], f"{curr_path}[{i}]")
|
|
73
73
|
for i in range(min_len, max(len_src, len_trg)):
|
|
74
74
|
comp["added" if len_src > len_trg else "removed"].append(
|
|
75
|
-
f"{curr_path}[{i}]"
|
|
75
|
+
f"{curr_path}[{i}]",
|
|
76
76
|
)
|
|
77
77
|
return
|
|
78
78
|
comp["unchanged" if src == trg else "modified"].append(curr_path)
|
|
@@ -4,5 +4,7 @@ All exceptions inherit from :class:`PySerialsException`, which itself
|
|
|
4
4
|
extends ``exceptionman.ReporterException`` for rich error reporting.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
__all__ = ["PySerialsException", "read", "update", "validate"]
|
|
8
|
+
|
|
8
9
|
from pyserials.exception import read, update, validate
|
|
10
|
+
from pyserials.exception._base import PySerialsException
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING as _TYPE_CHECKING
|
|
6
5
|
from functools import partial as _partial
|
|
6
|
+
from typing import TYPE_CHECKING as _TYPE_CHECKING
|
|
7
7
|
|
|
8
|
-
from exceptionman import ReporterException as _ReporterException # type: ignore[import-untyped]
|
|
9
8
|
import mdit as _mdit # type: ignore[import-untyped]
|
|
9
|
+
from exceptionman import (
|
|
10
|
+
ReporterException as _ReporterException, # type: ignore[import-untyped]
|
|
11
|
+
)
|
|
10
12
|
|
|
11
13
|
if _TYPE_CHECKING:
|
|
12
14
|
from mdit import Document
|
|
@@ -31,8 +33,7 @@ class PySerialsException(_ReporterException): # type: ignore[misc]
|
|
|
31
33
|
renderer=_partial(
|
|
32
34
|
_mdit.render.sphinx,
|
|
33
35
|
config=_mdit.render.get_sphinx_config(sphinx_config),
|
|
34
|
-
)
|
|
36
|
+
),
|
|
35
37
|
)
|
|
36
38
|
report.target_configs["sphinx"] = sphinx_target_config
|
|
37
39
|
super().__init__(report=report)
|
|
38
|
-
return
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"""Exceptions raised by `pyserials.read` module."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
from typing import Literal as _Literal
|
|
5
|
-
from pathlib import Path as _Path
|
|
6
4
|
|
|
7
|
-
import ruamel.yaml as _yaml
|
|
8
|
-
from ruamel.yaml.error import MarkedYAMLError as _MarkedYAMLError
|
|
9
5
|
import json as _json
|
|
10
|
-
import
|
|
6
|
+
from pathlib import Path as _Path
|
|
7
|
+
from typing import Literal as _Literal
|
|
11
8
|
|
|
9
|
+
import mdit as _mdit # type: ignore[import-untyped]
|
|
10
|
+
from ruamel.yaml.error import MarkedYAMLError as _MarkedYAMLError
|
|
12
11
|
from tomlkit.exceptions import TOMLKitError as _TOMLKitError
|
|
13
12
|
|
|
14
13
|
from pyserials.exception import _base
|
|
@@ -27,7 +26,7 @@ class PySerialsReadException(_base.PySerialsException):
|
|
|
27
26
|
Path to the input datafile, if data was read from a file.
|
|
28
27
|
"""
|
|
29
28
|
|
|
30
|
-
__slots__ = ("
|
|
29
|
+
__slots__ = ("data_type", "filepath", "source_type")
|
|
31
30
|
|
|
32
31
|
def __init__(
|
|
33
32
|
self,
|
|
@@ -44,7 +43,9 @@ class PySerialsReadException(_base.PySerialsException):
|
|
|
44
43
|
"string."
|
|
45
44
|
if source_type == "string"
|
|
46
45
|
else _mdit.inline_container(
|
|
47
|
-
"file at ",
|
|
46
|
+
"file at ",
|
|
47
|
+
_mdit.element.code_span(filepath),
|
|
48
|
+
".",
|
|
48
49
|
),
|
|
49
50
|
separator=" ",
|
|
50
51
|
)
|
|
@@ -60,7 +61,6 @@ class PySerialsReadException(_base.PySerialsException):
|
|
|
60
61
|
self.source_type: _Literal["file", "string"] = source_type
|
|
61
62
|
self.data_type: _Literal["json", "yaml", "toml"] | None = data_type
|
|
62
63
|
self.filepath: _Path | None = filepath
|
|
63
|
-
return
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
class PySerialsEmptyStringError(PySerialsReadException):
|
|
@@ -71,7 +71,6 @@ class PySerialsEmptyStringError(PySerialsReadException):
|
|
|
71
71
|
def __init__(self, data_type: _Literal["json", "yaml", "toml"]) -> None:
|
|
72
72
|
problem = "The string is empty."
|
|
73
73
|
super().__init__(problem=problem, source_type="string", data_type=data_type)
|
|
74
|
-
return
|
|
75
74
|
|
|
76
75
|
|
|
77
76
|
class PySerialsInvalidFileExtensionError(PySerialsReadException):
|
|
@@ -91,10 +90,10 @@ class PySerialsInvalidFileExtensionError(PySerialsReadException):
|
|
|
91
90
|
_mdit.element.code_span(".toml"),
|
|
92
91
|
", but got ",
|
|
93
92
|
_mdit.element.code_span(str(filepath.suffix.removeprefix("."))),
|
|
94
|
-
". Please provide the extension explicitly,
|
|
93
|
+
". Please provide the extension explicitly,"
|
|
94
|
+
" or rename the file to have a valid extension.",
|
|
95
95
|
)
|
|
96
96
|
super().__init__(problem=problem, source_type="file", filepath=filepath)
|
|
97
|
-
return
|
|
98
97
|
|
|
99
98
|
|
|
100
99
|
class PySerialsMissingFileError(PySerialsReadException):
|
|
@@ -103,13 +102,17 @@ class PySerialsMissingFileError(PySerialsReadException):
|
|
|
103
102
|
__slots__ = ()
|
|
104
103
|
|
|
105
104
|
def __init__(
|
|
106
|
-
self,
|
|
105
|
+
self,
|
|
106
|
+
data_type: _Literal["json", "yaml", "toml"],
|
|
107
|
+
filepath: _Path,
|
|
107
108
|
) -> None:
|
|
108
109
|
problem: str = "The file does not exist."
|
|
109
110
|
super().__init__(
|
|
110
|
-
problem=problem,
|
|
111
|
+
problem=problem,
|
|
112
|
+
source_type="file",
|
|
113
|
+
data_type=data_type,
|
|
114
|
+
filepath=filepath,
|
|
111
115
|
)
|
|
112
|
-
return
|
|
113
116
|
|
|
114
117
|
|
|
115
118
|
class PySerialsEmptyFileError(PySerialsReadException):
|
|
@@ -118,13 +121,17 @@ class PySerialsEmptyFileError(PySerialsReadException):
|
|
|
118
121
|
__slots__ = ()
|
|
119
122
|
|
|
120
123
|
def __init__(
|
|
121
|
-
self,
|
|
124
|
+
self,
|
|
125
|
+
data_type: _Literal["json", "yaml", "toml"],
|
|
126
|
+
filepath: _Path,
|
|
122
127
|
) -> None:
|
|
123
128
|
problem: str = "The file is empty."
|
|
124
129
|
super().__init__(
|
|
125
|
-
problem=problem,
|
|
130
|
+
problem=problem,
|
|
131
|
+
source_type="file",
|
|
132
|
+
data_type=data_type,
|
|
133
|
+
filepath=filepath,
|
|
126
134
|
)
|
|
127
|
-
return
|
|
128
135
|
|
|
129
136
|
|
|
130
137
|
class PySerialsInvalidDataError(PySerialsReadException):
|
|
@@ -137,16 +144,16 @@ class PySerialsInvalidDataError(PySerialsReadException):
|
|
|
137
144
|
"""
|
|
138
145
|
|
|
139
146
|
__slots__ = (
|
|
140
|
-
"data",
|
|
141
147
|
"cause",
|
|
142
|
-
"problem",
|
|
143
|
-
"problem_line",
|
|
144
|
-
"problem_column",
|
|
145
|
-
"problem_data_type",
|
|
146
148
|
"context",
|
|
147
|
-
"context_line",
|
|
148
149
|
"context_column",
|
|
149
150
|
"context_data_type",
|
|
151
|
+
"context_line",
|
|
152
|
+
"data",
|
|
153
|
+
"problem",
|
|
154
|
+
"problem_column",
|
|
155
|
+
"problem_data_type",
|
|
156
|
+
"problem_line",
|
|
150
157
|
)
|
|
151
158
|
|
|
152
159
|
def __init__(
|
|
@@ -173,7 +180,7 @@ class PySerialsInvalidDataError(PySerialsReadException):
|
|
|
173
180
|
self.problem_line = cause.problem_mark.line + 1
|
|
174
181
|
self.problem_column = cause.problem_mark.column + 1
|
|
175
182
|
self.problem_data_type = cause.problem_mark.name.removeprefix(
|
|
176
|
-
"<"
|
|
183
|
+
"<",
|
|
177
184
|
).removesuffix(">")
|
|
178
185
|
self.problem = cause.problem.strip()
|
|
179
186
|
if cause.context:
|
|
@@ -182,7 +189,7 @@ class PySerialsInvalidDataError(PySerialsReadException):
|
|
|
182
189
|
self.context_line = cause.context_mark.line + 1
|
|
183
190
|
self.context_column = cause.context_mark.column + 1
|
|
184
191
|
self.context_data_type = cause.context_mark.name.removeprefix(
|
|
185
|
-
"<"
|
|
192
|
+
"<",
|
|
186
193
|
).removesuffix(">")
|
|
187
194
|
elif isinstance(cause, _json.JSONDecodeError):
|
|
188
195
|
self.problem = cause.msg
|
|
@@ -192,7 +199,7 @@ class PySerialsInvalidDataError(PySerialsReadException):
|
|
|
192
199
|
self.problem_line = getattr(cause, "line", None)
|
|
193
200
|
self.problem_column = getattr(cause, "col", None)
|
|
194
201
|
self.problem = cause.args[0].removesuffix(
|
|
195
|
-
f" at line {self.problem_line} col {self.problem_column}"
|
|
202
|
+
f" at line {self.problem_line} col {self.problem_column}",
|
|
196
203
|
)
|
|
197
204
|
self.problem = self.problem.strip().capitalize().removesuffix(".")
|
|
198
205
|
description = ["The data is not valid"]
|
|
@@ -203,7 +210,7 @@ class PySerialsInvalidDataError(PySerialsReadException):
|
|
|
203
210
|
_mdit.element.code_span(str(self.problem_line)),
|
|
204
211
|
", column ",
|
|
205
212
|
_mdit.element.code_span(str(self.problem_column)),
|
|
206
|
-
]
|
|
213
|
+
],
|
|
207
214
|
)
|
|
208
215
|
description.append(f": {self.problem}.")
|
|
209
216
|
super().__init__(
|
|
@@ -213,7 +220,6 @@ class PySerialsInvalidDataError(PySerialsReadException):
|
|
|
213
220
|
data_type=data_type,
|
|
214
221
|
filepath=filepath,
|
|
215
222
|
)
|
|
216
|
-
return
|
|
217
223
|
|
|
218
224
|
def _report_content(self) -> dict[str, _mdit.element.Admonition]:
|
|
219
225
|
|
|
@@ -245,7 +251,7 @@ class PySerialsInvalidDataError(PySerialsReadException):
|
|
|
245
251
|
self.problem_data_type,
|
|
246
252
|
),
|
|
247
253
|
type="error",
|
|
248
|
-
)
|
|
254
|
+
),
|
|
249
255
|
}
|
|
250
256
|
if self.context:
|
|
251
257
|
content["context_details"] = _mdit.element.admonition(
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Exceptions raised by `pyserials.update` module."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
from typing import Any as _Any
|
|
6
|
+
from typing import Literal as _Literal
|
|
5
7
|
|
|
6
8
|
import mdit as _mdit # type: ignore[import-untyped]
|
|
7
9
|
|
|
@@ -21,18 +23,21 @@ class PySerialsUpdateException(_base.PySerialsException):
|
|
|
21
23
|
Full data input.
|
|
22
24
|
"""
|
|
23
25
|
|
|
24
|
-
__slots__ = ("
|
|
26
|
+
__slots__ = ("data", "data_full", "path")
|
|
25
27
|
|
|
26
28
|
def __init__(
|
|
27
29
|
self,
|
|
28
30
|
path: str,
|
|
29
31
|
data: object,
|
|
30
|
-
|
|
32
|
+
*,
|
|
33
|
+
data_full: dict[str, _Any] | list[_Any] | str | float | bool,
|
|
31
34
|
problem: object,
|
|
32
35
|
section: dict[str, object] | None = None,
|
|
33
36
|
) -> None:
|
|
34
37
|
intro = _mdit.inline_container(
|
|
35
|
-
"Failed to update data at ",
|
|
38
|
+
"Failed to update data at ",
|
|
39
|
+
_mdit.element.code_span(path),
|
|
40
|
+
".",
|
|
36
41
|
)
|
|
37
42
|
report = _mdit.document(
|
|
38
43
|
heading="Data Update Error",
|
|
@@ -46,7 +51,6 @@ class PySerialsUpdateException(_base.PySerialsException):
|
|
|
46
51
|
self.path = path
|
|
47
52
|
self.data = data
|
|
48
53
|
self.data_full = data_full
|
|
49
|
-
return
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
class PySerialsUpdateRecursiveDataError(PySerialsUpdateException):
|
|
@@ -61,11 +65,11 @@ class PySerialsUpdateRecursiveDataError(PySerialsUpdateException):
|
|
|
61
65
|
"""
|
|
62
66
|
|
|
63
67
|
__slots__ = (
|
|
64
|
-
"type_data",
|
|
65
|
-
"type_data_addon",
|
|
66
|
-
"problem_type",
|
|
67
68
|
"data_addon",
|
|
68
69
|
"data_addon_full",
|
|
70
|
+
"problem_type",
|
|
71
|
+
"type_data",
|
|
72
|
+
"type_data_addon",
|
|
69
73
|
)
|
|
70
74
|
|
|
71
75
|
def __init__(
|
|
@@ -88,7 +92,8 @@ class PySerialsUpdateRecursiveDataError(PySerialsUpdateException):
|
|
|
88
92
|
)
|
|
89
93
|
if problem_type == "duplicate"
|
|
90
94
|
else _mdit.inline_container(
|
|
91
|
-
"There was a type mismatch between the source
|
|
95
|
+
"There was a type mismatch between the source"
|
|
96
|
+
" and addon dictionary values: ",
|
|
92
97
|
"the value is of type ",
|
|
93
98
|
_mdit.element.code_span(self.type_data.__name__),
|
|
94
99
|
" in the source data, ",
|
|
@@ -106,7 +111,6 @@ class PySerialsUpdateRecursiveDataError(PySerialsUpdateException):
|
|
|
106
111
|
self.problem_type: _Literal["duplicate", "type_mismatch"] = problem_type
|
|
107
112
|
self.data_addon = data_addon
|
|
108
113
|
self.data_addon_full = data_addon_full
|
|
109
|
-
return
|
|
110
114
|
|
|
111
115
|
|
|
112
116
|
class PySerialsUpdateTemplatedDataError(PySerialsUpdateException):
|
|
@@ -124,7 +128,7 @@ class PySerialsUpdateTemplatedDataError(PySerialsUpdateException):
|
|
|
124
128
|
The end marker of the template.
|
|
125
129
|
"""
|
|
126
130
|
|
|
127
|
-
__slots__ = ("
|
|
131
|
+
__slots__ = ("data_source", "path_invalid", "template_end", "template_start")
|
|
128
132
|
|
|
129
133
|
def __init__(
|
|
130
134
|
self,
|
|
@@ -132,7 +136,8 @@ class PySerialsUpdateTemplatedDataError(PySerialsUpdateException):
|
|
|
132
136
|
path_invalid: str | object,
|
|
133
137
|
path: str | object,
|
|
134
138
|
data: object,
|
|
135
|
-
|
|
139
|
+
*,
|
|
140
|
+
data_full: dict[str, _Any] | list[_Any] | str | float | bool,
|
|
136
141
|
data_source: dict[str, _Any] | list[_Any],
|
|
137
142
|
template_start: str,
|
|
138
143
|
template_end: str,
|
|
@@ -150,4 +155,3 @@ class PySerialsUpdateTemplatedDataError(PySerialsUpdateException):
|
|
|
150
155
|
data_full=data_full,
|
|
151
156
|
problem=_mdit.inline_container(*parts),
|
|
152
157
|
)
|
|
153
|
-
return
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Exceptions raised by `pyserials.validate` module."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
from typing import Any as _Any
|
|
5
6
|
|
|
6
7
|
import jsonschema as _jsonschema # type: ignore[import-untyped]
|
|
@@ -24,12 +25,13 @@ class PySerialsValidateException(_base.PySerialsException):
|
|
|
24
25
|
The validator that was used to validate the data against the schema.
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
|
-
__slots__ = ("data", "
|
|
28
|
+
__slots__ = ("data", "registry", "schema", "validator")
|
|
28
29
|
|
|
29
30
|
def __init__(
|
|
30
31
|
self,
|
|
31
32
|
problem: str | object,
|
|
32
|
-
|
|
33
|
+
*,
|
|
34
|
+
data: dict[str, _Any] | list[_Any] | str | float | bool,
|
|
33
35
|
schema: dict[str, _Any],
|
|
34
36
|
validator: _Any,
|
|
35
37
|
registry: _Any = None,
|
|
@@ -53,7 +55,6 @@ class PySerialsValidateException(_base.PySerialsException):
|
|
|
53
55
|
self.schema = schema
|
|
54
56
|
self.validator = validator
|
|
55
57
|
self.registry = registry
|
|
56
|
-
return
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
class PySerialsInvalidJsonSchemaError(PySerialsValidateException):
|
|
@@ -63,7 +64,8 @@ class PySerialsInvalidJsonSchemaError(PySerialsValidateException):
|
|
|
63
64
|
|
|
64
65
|
def __init__(
|
|
65
66
|
self,
|
|
66
|
-
|
|
67
|
+
*,
|
|
68
|
+
data: dict[str, _Any] | list[_Any] | str | float | bool,
|
|
67
69
|
schema: dict[str, _Any],
|
|
68
70
|
validator: _Any,
|
|
69
71
|
registry: _Any = None,
|
|
@@ -75,18 +77,18 @@ class PySerialsInvalidJsonSchemaError(PySerialsValidateException):
|
|
|
75
77
|
validator=validator,
|
|
76
78
|
registry=registry,
|
|
77
79
|
)
|
|
78
|
-
return
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
class PySerialsJsonSchemaValidationError(PySerialsValidateException):
|
|
82
|
-
"""
|
|
83
|
+
"""Raised when the data does not satisfy its JSON Schema."""
|
|
83
84
|
|
|
84
85
|
__slots__ = ("causes",)
|
|
85
86
|
|
|
86
87
|
def __init__(
|
|
87
88
|
self,
|
|
88
89
|
causes: list[_jsonschema.exceptions.ValidationError],
|
|
89
|
-
|
|
90
|
+
*,
|
|
91
|
+
data: dict[str, _Any] | list[_Any] | str | float | bool,
|
|
90
92
|
schema: dict[str, _Any],
|
|
91
93
|
validator: _Any,
|
|
92
94
|
registry: _Any = None,
|
|
@@ -103,22 +105,49 @@ class PySerialsJsonSchemaValidationError(PySerialsValidateException):
|
|
|
103
105
|
validator=validator,
|
|
104
106
|
registry=registry,
|
|
105
107
|
)
|
|
106
|
-
|
|
108
|
+
|
|
109
|
+
def __str__(self) -> str:
|
|
110
|
+
n = len(self.causes)
|
|
111
|
+
plural = "s" if n != 1 else ""
|
|
112
|
+
lines = [f"Schema validation failed: {n} error{plural} found."]
|
|
113
|
+
for i, error in enumerate(self.causes, 1):
|
|
114
|
+
lines.extend(self._format_error_text(error, indent=2, prefix=f"[{i}]"))
|
|
115
|
+
return "\n".join(lines)
|
|
107
116
|
|
|
108
117
|
def _generate_problem_statement(self) -> _mdit.container.MDContainer:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"an error " if count_errors == 1 else f"{count_errors} errors ",
|
|
117
|
-
"in the data at ",
|
|
118
|
-
error_paths_str,
|
|
119
|
-
".",
|
|
118
|
+
n = len(self.causes)
|
|
119
|
+
plural = "s" if n != 1 else ""
|
|
120
|
+
header = _mdit.inline_container(f"Found {n} error{plural}:")
|
|
121
|
+
list_items = [self._make_error_item(error) for error in self.causes]
|
|
122
|
+
return _mdit.block_container(
|
|
123
|
+
header,
|
|
124
|
+
_mdit.element.unordered_list(list_items),
|
|
120
125
|
)
|
|
121
|
-
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def _context_depth(cls, error: _jsonschema.exceptions.ValidationError) -> int:
|
|
129
|
+
"""Return the maximum depth of an error's context tree (0 for leaf errors)."""
|
|
130
|
+
if not error.context:
|
|
131
|
+
return 0
|
|
132
|
+
return 1 + max(cls._context_depth(sub) for sub in error.context)
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def _make_error_item(
|
|
136
|
+
cls,
|
|
137
|
+
error: _jsonschema.exceptions.ValidationError,
|
|
138
|
+
) -> _mdit.container.MDContainer:
|
|
139
|
+
"""Recursively build an mdit item for one error, nesting context sub-errors."""
|
|
140
|
+
path = _mdit.element.code_span(error.json_path or "$")
|
|
141
|
+
msg = cls._parse_error_message(error)
|
|
142
|
+
item = _mdit.inline_container(path, " — ", msg)
|
|
143
|
+
if error.context:
|
|
144
|
+
sorted_context = sorted(error.context, key=cls._context_depth)
|
|
145
|
+
sub_items = [cls._make_error_item(sub) for sub in sorted_context]
|
|
146
|
+
item = _mdit.block_container(
|
|
147
|
+
item,
|
|
148
|
+
_mdit.element.unordered_list(sub_items),
|
|
149
|
+
)
|
|
150
|
+
return item
|
|
122
151
|
|
|
123
152
|
def _generate_error_report(
|
|
124
153
|
self,
|
|
@@ -157,7 +186,7 @@ class PySerialsJsonSchemaValidationError(PySerialsValidateException):
|
|
|
157
186
|
if error.context:
|
|
158
187
|
context_paths = []
|
|
159
188
|
for idx, sub_error in enumerate(
|
|
160
|
-
sorted(error.context, key=
|
|
189
|
+
sorted(error.context, key=self._context_depth),
|
|
161
190
|
):
|
|
162
191
|
section[str(idx)] = self._generate_error_report(sub_error)
|
|
163
192
|
context_paths.append(_mdit.element.code_span(sub_error.json_path))
|
|
@@ -177,7 +206,7 @@ class PySerialsJsonSchemaValidationError(PySerialsValidateException):
|
|
|
177
206
|
full_ver_items[0].append(
|
|
178
207
|
f" This was caused by {err} at {context_paths_joined}.",
|
|
179
208
|
)
|
|
180
|
-
|
|
209
|
+
return _mdit.document(
|
|
181
210
|
heading=_mdit.element.code_span(error.json_path),
|
|
182
211
|
body={
|
|
183
212
|
"short": (
|
|
@@ -188,25 +217,45 @@ class PySerialsJsonSchemaValidationError(PySerialsValidateException):
|
|
|
188
217
|
},
|
|
189
218
|
section=section,
|
|
190
219
|
)
|
|
191
|
-
return doc
|
|
192
220
|
|
|
193
221
|
@staticmethod
|
|
194
222
|
def _make_yaml_code_admo(
|
|
195
|
-
admo_type: str,
|
|
223
|
+
admo_type: str,
|
|
224
|
+
title: str,
|
|
225
|
+
title_details: str,
|
|
226
|
+
content: dict[str, _Any],
|
|
196
227
|
) -> _mdit.element.Admonition:
|
|
197
228
|
code_block = _mdit.element.code_block(
|
|
198
229
|
content=_write.to_yaml_string(content, end_of_file_newline=False),
|
|
199
230
|
language="yaml",
|
|
200
231
|
)
|
|
201
|
-
|
|
232
|
+
return _mdit.element.admonition(
|
|
202
233
|
title=_mdit.inline_container(
|
|
203
|
-
f"**{title}**: ",
|
|
234
|
+
f"**{title}**: ",
|
|
235
|
+
_mdit.element.code_span(title_details),
|
|
204
236
|
),
|
|
205
237
|
body=code_block,
|
|
206
238
|
type=admo_type,
|
|
207
239
|
dropdown=True,
|
|
208
240
|
)
|
|
209
|
-
|
|
241
|
+
|
|
242
|
+
@classmethod
|
|
243
|
+
def _format_error_text(
|
|
244
|
+
cls,
|
|
245
|
+
error: _jsonschema.exceptions.ValidationError,
|
|
246
|
+
indent: int = 0,
|
|
247
|
+
prefix: str = "",
|
|
248
|
+
) -> list[str]:
|
|
249
|
+
"""Recursively format a ValidationError as indented plain-text lines."""
|
|
250
|
+
pad = " " * indent
|
|
251
|
+
path = error.json_path or "$"
|
|
252
|
+
label = f"{prefix} " if prefix else ""
|
|
253
|
+
lines = [f"{pad}{label}{path}: {error.message}"]
|
|
254
|
+
for j, sub in enumerate(sorted(error.context, key=cls._context_depth)):
|
|
255
|
+
lines.extend(
|
|
256
|
+
cls._format_error_text(sub, indent=indent + 4, prefix=f"[{j}]"),
|
|
257
|
+
)
|
|
258
|
+
return lines
|
|
210
259
|
|
|
211
260
|
@staticmethod
|
|
212
261
|
def _parse_error_message(error: _jsonschema.exceptions.ValidationError) -> str:
|
|
@@ -231,7 +280,7 @@ class PySerialsJsonSchemaValidationError(PySerialsValidateException):
|
|
|
231
280
|
) -> Stringable:
|
|
232
281
|
if len(items) == 1:
|
|
233
282
|
return items[0]
|
|
234
|
-
|
|
283
|
+
if len(items) == 2:
|
|
235
284
|
return _mdit.inline_container(items[0], sep_pair, items[1])
|
|
236
285
|
container: list[_Any] = []
|
|
237
286
|
for item in items[:-1]:
|
|
@@ -87,12 +87,14 @@ def _flatten(
|
|
|
87
87
|
return output
|
|
88
88
|
if len(data) != 1:
|
|
89
89
|
num_non_prim = sum(
|
|
90
|
-
1
|
|
90
|
+
1
|
|
91
|
+
for v in data
|
|
92
|
+
if not isinstance(v, (str, int, float, bool, type(None)))
|
|
91
93
|
)
|
|
92
94
|
raise ValueError(
|
|
93
|
-
f"Data at '{path or 'root'}' contains a list with {len(data)}
|
|
94
|
-
f"({num_non_prim} non-primitive); only a list containing
|
|
95
|
-
f"non-primitive element is supported: {data}"
|
|
95
|
+
f"Data at '{path or 'root'}' contains a list with {len(data)}"
|
|
96
|
+
f" element(s) ({num_non_prim} non-primitive); only a list containing"
|
|
97
|
+
f" exactly one non-primitive element is supported: {data}",
|
|
96
98
|
)
|
|
97
99
|
return _flatten(data=data[0], output=output, path=path)
|
|
98
100
|
raise ValueError(f"Unknown data type {type(data)} at {path}")
|