PySerials 0.0.0.dev4__tar.gz → 0.0.0.dev6__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.0.0.dev4 → pyserials-0.0.0.dev6}/PKG-INFO +3 -2
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/pyproject.toml +4 -3
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/PySerials.egg-info/PKG-INFO +3 -2
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/PySerials.egg-info/requires.txt +2 -1
- pyserials-0.0.0.dev6/src/pyserials/exception/_base.py +26 -0
- pyserials-0.0.0.dev6/src/pyserials/exception/read.py +240 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/exception/update.py +37 -38
- pyserials-0.0.0.dev6/src/pyserials/exception/validate.py +216 -0
- pyserials-0.0.0.dev4/src/pyserials/exception/_base.py +0 -58
- pyserials-0.0.0.dev4/src/pyserials/exception/read.py +0 -211
- pyserials-0.0.0.dev4/src/pyserials/exception/validate.py +0 -243
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/README.md +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/setup.cfg +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/PySerials.egg-info/SOURCES.txt +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/PySerials.egg-info/dependency_links.txt +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/PySerials.egg-info/not-zip-safe +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/PySerials.egg-info/top_level.txt +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/__init__.py +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/compare.py +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/exception/__init__.py +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/format.py +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/nested_dict.py +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/read.py +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/update.py +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/validate.py +0 -0
- {pyserials-0.0.0.dev4 → pyserials-0.0.0.dev6}/src/pyserials/write.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PySerials
|
|
3
|
-
Version: 0.0.0.
|
|
3
|
+
Version: 0.0.0.dev6
|
|
4
4
|
Requires-Python: >=3.10
|
|
5
5
|
Requires-Dist: jsonschema<5,>=4.21.0
|
|
6
6
|
Requires-Dist: ruamel.yaml<0.18,>=0.17.32
|
|
7
7
|
Requires-Dist: ruamel.yaml.string<1,>=0.1.1
|
|
8
8
|
Requires-Dist: tomlkit<0.12,>=0.11.8
|
|
9
|
-
Requires-Dist:
|
|
9
|
+
Requires-Dist: mdit
|
|
10
10
|
Requires-Dist: ansi-sgr
|
|
11
11
|
Requires-Dist: ExceptionMan
|
|
12
|
+
Requires-Dist: jsonpath-ng<2,>=1.6.1
|
|
@@ -17,15 +17,16 @@ namespaces = true
|
|
|
17
17
|
# ----------------------------------------- Project Metadata -------------------------------------
|
|
18
18
|
#
|
|
19
19
|
[project]
|
|
20
|
-
version = "0.0.0.
|
|
20
|
+
version = "0.0.0.dev6"
|
|
21
21
|
name = "PySerials"
|
|
22
22
|
dependencies = [
|
|
23
23
|
"jsonschema >= 4.21.0, < 5",
|
|
24
24
|
"ruamel.yaml >= 0.17.32, < 0.18", # https://yaml.readthedocs.io/en/stable/
|
|
25
25
|
"ruamel.yaml.string >= 0.1.1, < 1",
|
|
26
26
|
"tomlkit >= 0.11.8, < 0.12", # https://tomlkit.readthedocs.io/en/stable/,
|
|
27
|
-
"
|
|
27
|
+
"mdit",
|
|
28
28
|
"ansi-sgr",
|
|
29
|
-
"ExceptionMan"
|
|
29
|
+
"ExceptionMan",
|
|
30
|
+
"jsonpath-ng >= 1.6.1, < 2",
|
|
30
31
|
]
|
|
31
32
|
requires-python = ">=3.10"
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: PySerials
|
|
3
|
-
Version: 0.0.0.
|
|
3
|
+
Version: 0.0.0.dev6
|
|
4
4
|
Requires-Python: >=3.10
|
|
5
5
|
Requires-Dist: jsonschema<5,>=4.21.0
|
|
6
6
|
Requires-Dist: ruamel.yaml<0.18,>=0.17.32
|
|
7
7
|
Requires-Dist: ruamel.yaml.string<1,>=0.1.1
|
|
8
8
|
Requires-Dist: tomlkit<0.12,>=0.11.8
|
|
9
|
-
Requires-Dist:
|
|
9
|
+
Requires-Dist: mdit
|
|
10
10
|
Requires-Dist: ansi-sgr
|
|
11
11
|
Requires-Dist: ExceptionMan
|
|
12
|
+
Requires-Dist: jsonpath-ng<2,>=1.6.1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""PySerials base Exception class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING as _TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from exceptionman import ReporterException as _ReporterException
|
|
8
|
+
|
|
9
|
+
if _TYPE_CHECKING:
|
|
10
|
+
from mdit import Document
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PySerialsException(_ReporterException):
|
|
14
|
+
"""Base class for all exceptions raised by PySerials."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, report: Document):
|
|
17
|
+
super().__init__(
|
|
18
|
+
report=report,
|
|
19
|
+
sphinx_config={
|
|
20
|
+
"extensions": ['myst_parser', 'sphinx_togglebutton'],
|
|
21
|
+
"myst_enable_extensions": ["colon_fence", "fieldlist"],
|
|
22
|
+
"html_theme": "pydata_sphinx_theme",
|
|
23
|
+
"html_title": "PySerials Error Report",
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
return
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Exceptions raised by `pyserials.read` module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Literal as _Literal, TYPE_CHECKING as _TYPE_CHECKING
|
|
5
|
+
from pathlib import Path as _Path
|
|
6
|
+
|
|
7
|
+
import ruamel.yaml as _yaml
|
|
8
|
+
import json as _json
|
|
9
|
+
import mdit as _mdit
|
|
10
|
+
|
|
11
|
+
from tomlkit.exceptions import TOMLKitError as _TOMLKitError
|
|
12
|
+
|
|
13
|
+
from pyserials.exception import _base
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PySerialsReadException(_base.PySerialsException):
|
|
17
|
+
"""Base class for all exceptions raised by `pyserials.read` module.
|
|
18
|
+
|
|
19
|
+
Attributes
|
|
20
|
+
----------
|
|
21
|
+
source_type : {"file", "string"}
|
|
22
|
+
Type of source from which data was read.
|
|
23
|
+
data_type : {"json", "yaml", "toml"} or None
|
|
24
|
+
Type of input data, if known.
|
|
25
|
+
filepath : pathlib.Path or None
|
|
26
|
+
Path to the input datafile, if data was read from a file.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
source_type: _Literal["file", "string"],
|
|
32
|
+
problem,
|
|
33
|
+
section: dict | None = None,
|
|
34
|
+
data_type: _Literal["json", "yaml", "toml"] | None = None,
|
|
35
|
+
filepath: _Path | None = None,
|
|
36
|
+
):
|
|
37
|
+
intro = _mdit.inline_container(
|
|
38
|
+
"Failed to read ",
|
|
39
|
+
f"{data_type.upper()} data " if data_type else "data ",
|
|
40
|
+
"from input ",
|
|
41
|
+
"string." if source_type == "string" else _mdit.inline_container(
|
|
42
|
+
"file at ", _mdit.element.code_span(filepath), "."
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
report = _mdit.document(
|
|
46
|
+
heading="Data Read Error",
|
|
47
|
+
body={
|
|
48
|
+
"intro": intro,
|
|
49
|
+
"problem": problem,
|
|
50
|
+
},
|
|
51
|
+
section=section,
|
|
52
|
+
)
|
|
53
|
+
super().__init__(report)
|
|
54
|
+
self.source_type: _Literal["file", "string"] = source_type
|
|
55
|
+
self.data_type: _Literal["json", "yaml", "toml"] | None = data_type
|
|
56
|
+
self.filepath: _Path | None = filepath
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PySerialsEmptyStringError(PySerialsReadException):
|
|
61
|
+
"""Exception raised when a string to be read is empty."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, data_type: _Literal["json", "yaml", "toml"]):
|
|
64
|
+
problem = f"The string is empty."
|
|
65
|
+
super().__init__(problem=problem, source_type="string", data_type=data_type)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class PySerialsInvalidFileExtensionError(PySerialsReadException):
|
|
70
|
+
"""Exception raised when a file to be read has an unrecognized extension."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, filepath: _Path):
|
|
73
|
+
problem = _mdit.inline_container(
|
|
74
|
+
"The file extension must be one of ",
|
|
75
|
+
_mdit.element.code_span('json'),
|
|
76
|
+
", ",
|
|
77
|
+
_mdit.element.code_span('yaml'),
|
|
78
|
+
", ",
|
|
79
|
+
_mdit.element.code_span('yml'),
|
|
80
|
+
", or ",
|
|
81
|
+
_mdit.element.code_span('.toml'),
|
|
82
|
+
", but got ",
|
|
83
|
+
_mdit.element.code_span(str(filepath.suffix.removeprefix('.'))),
|
|
84
|
+
". Please provide the extension explicitly, or rename the file to have a valid extension."
|
|
85
|
+
)
|
|
86
|
+
super().__init__(problem=problem, source_type="file", filepath=filepath)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class PySerialsMissingFileError(PySerialsReadException):
|
|
91
|
+
"""Exception raised when a file to be read does not exist."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
|
|
94
|
+
problem = f"The file does not exist."
|
|
95
|
+
super().__init__(problem=problem, source_type="file", data_type=data_type, filepath=filepath)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class PySerialsEmptyFileError(PySerialsReadException):
|
|
100
|
+
"""Exception raised when a file to be read is empty."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
|
|
103
|
+
problem = f"The file is empty."
|
|
104
|
+
super().__init__(problem=problem, source_type="file", data_type=data_type, filepath=filepath)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PySerialsInvalidDataError(PySerialsReadException):
|
|
109
|
+
"""Exception raised when the data is invalid.
|
|
110
|
+
|
|
111
|
+
Attributes
|
|
112
|
+
----------
|
|
113
|
+
data : str
|
|
114
|
+
The input data that was supposed to be read.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(
|
|
118
|
+
self,
|
|
119
|
+
source_type: _Literal["file", "string"],
|
|
120
|
+
data_type: _Literal["json", "yaml", "toml"],
|
|
121
|
+
data: str,
|
|
122
|
+
cause: Exception,
|
|
123
|
+
filepath: _Path | None = None,
|
|
124
|
+
):
|
|
125
|
+
self.data = data
|
|
126
|
+
self.cause = cause
|
|
127
|
+
self.problem: str = str(cause)
|
|
128
|
+
self.problem_line: int | None = None
|
|
129
|
+
self.problem_column: int | None = None
|
|
130
|
+
self.problem_data_type: str | None = None
|
|
131
|
+
self.context: str | None = None
|
|
132
|
+
self.context_line: int | None = None
|
|
133
|
+
self.context_column: int | None = None
|
|
134
|
+
self.context_data_type: str | None = None
|
|
135
|
+
self.data_type = data_type
|
|
136
|
+
|
|
137
|
+
if isinstance(cause, _yaml.YAMLError):
|
|
138
|
+
self.problem_line = cause.problem_mark.line + 1
|
|
139
|
+
self.problem_column = cause.problem_mark.column + 1
|
|
140
|
+
self.problem_data_type = cause.problem_mark.name.removeprefix("<").removesuffix(">")
|
|
141
|
+
self.problem = cause.problem.strip()
|
|
142
|
+
if cause.context:
|
|
143
|
+
self.context = cause.context.strip()
|
|
144
|
+
self.context_line = cause.context_mark.line + 1
|
|
145
|
+
self.context_column = cause.context_mark.column + 1
|
|
146
|
+
self.context_data_type = cause.context_mark.name.removeprefix("<").removesuffix(">")
|
|
147
|
+
elif isinstance(cause, _json.JSONDecodeError):
|
|
148
|
+
self.problem = cause.msg
|
|
149
|
+
self.problem_line = cause.lineno
|
|
150
|
+
self.problem_column = cause.colno
|
|
151
|
+
elif isinstance(cause, _TOMLKitError):
|
|
152
|
+
self.problem_line = cause.line
|
|
153
|
+
self.problem_column = cause.col
|
|
154
|
+
self.problem = cause.args[0].removesuffix(f" at line {self.problem_line} col {self.problem_column}")
|
|
155
|
+
self.problem = self.problem.strip().capitalize().removesuffix(".")
|
|
156
|
+
description = ["The data is not valid"]
|
|
157
|
+
if self.problem_line:
|
|
158
|
+
description.extend(
|
|
159
|
+
[
|
|
160
|
+
" at line ",
|
|
161
|
+
_mdit.element.code_span(str(self.problem_line)),
|
|
162
|
+
", column ",
|
|
163
|
+
_mdit.element.code_span(str(self.problem_column)),
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
description.append(f": {self.problem}.")
|
|
167
|
+
super().__init__(
|
|
168
|
+
problem=description,
|
|
169
|
+
source_type=source_type,
|
|
170
|
+
section=self._report_content(),
|
|
171
|
+
data_type=data_type,
|
|
172
|
+
filepath=filepath
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
def _report_content(self) -> dict:
|
|
177
|
+
|
|
178
|
+
def make_tabel(problem, line, column, data_type):
|
|
179
|
+
items = [
|
|
180
|
+
_mdit.element.field_list_item(title=title, description=value) for title, value in [
|
|
181
|
+
["Description", problem],
|
|
182
|
+
["Line Number", line],
|
|
183
|
+
["Column Number", column],
|
|
184
|
+
["Data Type", data_type],
|
|
185
|
+
] if value is not None
|
|
186
|
+
]
|
|
187
|
+
return _mdit.element.field_list(items)
|
|
188
|
+
|
|
189
|
+
content = {
|
|
190
|
+
"problem_details": _mdit.element.admonition(
|
|
191
|
+
type="error",
|
|
192
|
+
title="Problem",
|
|
193
|
+
content=make_tabel(self.problem, self.problem_line, self.problem_column, self.problem_data_type)
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
if self.context:
|
|
197
|
+
content["context_details"] = _mdit.element.admonition(
|
|
198
|
+
type="note",
|
|
199
|
+
title="Context",
|
|
200
|
+
content=make_tabel(self.context, self.context_line, self.context_column, self.context_data_type)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
code_block_full = _mdit.element.code_block(
|
|
204
|
+
content=self.data,
|
|
205
|
+
language=self.data_type,
|
|
206
|
+
caption="Data",
|
|
207
|
+
line_num=True,
|
|
208
|
+
emphasize_lines=[line for line in (self.problem_line, self.context_line) if line],
|
|
209
|
+
degrade_to_diff=True,
|
|
210
|
+
)
|
|
211
|
+
content["data_full"] = (code_block_full, "full")
|
|
212
|
+
|
|
213
|
+
if not (self.problem_line or self.context_line):
|
|
214
|
+
code_block_short = _mdit.element.code_block(
|
|
215
|
+
content=self.data[:1000].strip() + "\n..." if len(self.data) > 1000 else self.data,
|
|
216
|
+
language=self.data_type,
|
|
217
|
+
caption="Data" if len(self.data) <= 1000 else "Data (truncated to first 1000 characters)",
|
|
218
|
+
line_num=True,
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
if self.problem_line and self.context_line:
|
|
222
|
+
line_start = min(self.problem_line, self.context_line)
|
|
223
|
+
line_end = max(self.problem_line, self.context_line)
|
|
224
|
+
else:
|
|
225
|
+
line_start = self.problem_line or self.context_line
|
|
226
|
+
line_end = line_start
|
|
227
|
+
data_lines = self.data.splitlines()
|
|
228
|
+
selected_lines = data_lines[line_start - 1:line_end]
|
|
229
|
+
code_block_short = _mdit.element.code_block(
|
|
230
|
+
content="\n".join(selected_lines),
|
|
231
|
+
language=self.data_type,
|
|
232
|
+
caption="Data",
|
|
233
|
+
line_num=True,
|
|
234
|
+
line_num_start=line_start,
|
|
235
|
+
emphasize_lines=list({1, len(selected_lines)}),
|
|
236
|
+
)
|
|
237
|
+
content["data_short"] = (code_block_short, ("short", "console"))
|
|
238
|
+
container = _mdit.block_container(**content)
|
|
239
|
+
doc = _mdit.document(heading="Error Details", body=container)
|
|
240
|
+
return {"details": doc}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
from typing import Any as _Any, Literal as _Literal
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import mdit as _mdit
|
|
7
7
|
|
|
8
8
|
from pyserials.exception import _base
|
|
9
9
|
|
|
@@ -26,18 +26,23 @@ class PySerialsUpdateException(_base.PySerialsException):
|
|
|
26
26
|
path: str,
|
|
27
27
|
data: dict | list | str | int | float | bool,
|
|
28
28
|
data_full: dict | list | str | int | float | bool,
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
problem,
|
|
30
|
+
section: dict | None = None,
|
|
31
31
|
):
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
intro = _mdit.inline_container(
|
|
33
|
+
"Failed to update data at ",
|
|
34
|
+
_mdit.element.code_span(path),
|
|
35
|
+
"."
|
|
36
|
+
)
|
|
37
|
+
report = _mdit.document(
|
|
38
|
+
heading="Data Update Error",
|
|
39
|
+
body={
|
|
40
|
+
"intro": intro,
|
|
41
|
+
"problem": problem,
|
|
42
|
+
},
|
|
43
|
+
section=section,
|
|
40
44
|
)
|
|
45
|
+
super().__init__(report)
|
|
41
46
|
self.path = path
|
|
42
47
|
self.data = data
|
|
43
48
|
self.data_full = data_full
|
|
@@ -66,31 +71,25 @@ class PySerialsUpdateDictFromAddonError(PySerialsUpdateException):
|
|
|
66
71
|
):
|
|
67
72
|
self.type_data = type(data)
|
|
68
73
|
self.type_data_addon = type(data_addon)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
"the value of type {type_data_addon} already exists in the source data."
|
|
83
|
-
) if problem_type == "duplicate" else (
|
|
84
|
-
"There was a type mismatch between the source and addon dictionary values; "
|
|
85
|
-
"the value is of type {type_data} in the source data, "
|
|
86
|
-
"but of type {type_data_addon} in the addon data."
|
|
74
|
+
problem = _mdit.inline_container(
|
|
75
|
+
"There was a duplicate in the addon dictionary: ",
|
|
76
|
+
"the value of type",
|
|
77
|
+
_mdit.element.code_span(self.type_data_addon.__name__),
|
|
78
|
+
" already exists in the source data."
|
|
79
|
+
) if problem_type == "duplicate" else _mdit.inline_container(
|
|
80
|
+
"There was a type mismatch between the source and addon dictionary values: ",
|
|
81
|
+
"the value is of type ",
|
|
82
|
+
_mdit.element.code_span(self.type_data.__name__),
|
|
83
|
+
" in the source data, ",
|
|
84
|
+
"but of type ",
|
|
85
|
+
_mdit.element.code_span(self.type_data_addon.__name__),
|
|
86
|
+
" in the addon data."
|
|
87
87
|
)
|
|
88
88
|
super().__init__(
|
|
89
|
-
description=description_template.format(**kwargs_console),
|
|
90
|
-
description_html=description_template.format(**kwargs_html),
|
|
91
89
|
path=path,
|
|
92
90
|
data=data,
|
|
93
91
|
data_full=data_full,
|
|
92
|
+
problem=problem,
|
|
94
93
|
)
|
|
95
94
|
self.problem_type: _Literal["duplicate", "type_mismatch"] = problem_type
|
|
96
95
|
self.data_addon = data_addon
|
|
@@ -124,16 +123,16 @@ class PySerialsUpdateTemplatedDataError(PySerialsUpdateException):
|
|
|
124
123
|
template_start: str,
|
|
125
124
|
template_end: str,
|
|
126
125
|
):
|
|
127
|
-
|
|
126
|
+
self.path_invalid = path_invalid.replace("'", "")
|
|
127
|
+
self.data_source = data_source
|
|
128
|
+
self.template_start = template_start
|
|
129
|
+
self.template_end = template_end
|
|
130
|
+
parts = description_template.split("{path_invalid}")
|
|
131
|
+
parts.insert(1, _mdit.element.code_span(self.path_invalid))
|
|
128
132
|
super().__init__(
|
|
129
|
-
description=description_template.format(path_invalid=path_invalid_console),
|
|
130
|
-
description_html=description_template.format(path_invalid=path_invalid_html),
|
|
131
133
|
path=path.replace("'", ""),
|
|
132
134
|
data=data,
|
|
133
135
|
data_full=data_full,
|
|
136
|
+
problem=_mdit.inline_container(*parts),
|
|
134
137
|
)
|
|
135
|
-
self.path_invalid = path_invalid
|
|
136
|
-
self.data_source = data_source
|
|
137
|
-
self.template_start = template_start
|
|
138
|
-
self.template_end = template_end
|
|
139
138
|
return
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Exceptions raised by `pyserials.validate` module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any as _Any
|
|
5
|
+
|
|
6
|
+
import jsonschema as _jsonschema
|
|
7
|
+
import mdit as _mdit
|
|
8
|
+
from pyprotocol import Stringable
|
|
9
|
+
|
|
10
|
+
from pyserials import write as _write
|
|
11
|
+
from pyserials.exception import _base
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PySerialsValidateException(_base.PySerialsException):
|
|
15
|
+
"""Base class for all exceptions raised by `pyserials.validate` module.
|
|
16
|
+
|
|
17
|
+
Attributes
|
|
18
|
+
----------
|
|
19
|
+
data : dict | list | str | int | float | bool
|
|
20
|
+
The data that failed validation.
|
|
21
|
+
schema : dict
|
|
22
|
+
The schema that the data failed to validate against.
|
|
23
|
+
validator : Any
|
|
24
|
+
The validator that was used to validate the data against the schema.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
problem: str,
|
|
30
|
+
data: dict | list | str | int | float | bool,
|
|
31
|
+
schema: dict,
|
|
32
|
+
validator: _Any,
|
|
33
|
+
registry: _Any = None,
|
|
34
|
+
section: dict | None = None,
|
|
35
|
+
):
|
|
36
|
+
intro = _mdit.inline_container(
|
|
37
|
+
"Failed to validate data against schema using validator ",
|
|
38
|
+
_mdit.element.code_span(validator.__class__.__name__),
|
|
39
|
+
"."
|
|
40
|
+
)
|
|
41
|
+
report = _mdit.document(
|
|
42
|
+
heading="Schema Validation Error",
|
|
43
|
+
body={
|
|
44
|
+
"intro": intro,
|
|
45
|
+
"problem": problem,
|
|
46
|
+
},
|
|
47
|
+
section=section,
|
|
48
|
+
)
|
|
49
|
+
super().__init__(report)
|
|
50
|
+
self.data = data
|
|
51
|
+
self.schema = schema
|
|
52
|
+
self.validator = validator
|
|
53
|
+
self.registry = registry
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PySerialsInvalidJsonSchemaError(PySerialsValidateException):
|
|
58
|
+
"""Exception raised when data validation fails due to the schema being invalid."""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
data: dict | list | str | int | float | bool,
|
|
63
|
+
schema: dict,
|
|
64
|
+
validator: _Any,
|
|
65
|
+
registry: _Any = None,
|
|
66
|
+
):
|
|
67
|
+
super().__init__(
|
|
68
|
+
problem="The schema is invalid.",
|
|
69
|
+
data=data,
|
|
70
|
+
schema=schema,
|
|
71
|
+
validator=validator,
|
|
72
|
+
registry=registry,
|
|
73
|
+
)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class PySerialsJsonSchemaValidationError(PySerialsValidateException):
|
|
78
|
+
"""Exception raised when data validation fails due to the data being invalid against the schema."""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
causes: list[_jsonschema.exceptions.ValidationError],
|
|
83
|
+
data: dict | list | str | int | float | bool,
|
|
84
|
+
schema: dict,
|
|
85
|
+
validator: _Any,
|
|
86
|
+
registry: _Any = None,
|
|
87
|
+
):
|
|
88
|
+
self.causes = causes
|
|
89
|
+
super().__init__(
|
|
90
|
+
problem=self._generate_problem_statement(),
|
|
91
|
+
section={idx: self._generate_error_report(error) for idx, error in enumerate(self.causes)},
|
|
92
|
+
data=data,
|
|
93
|
+
schema=schema,
|
|
94
|
+
validator=validator,
|
|
95
|
+
registry=registry,
|
|
96
|
+
)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
def _generate_problem_statement(self):
|
|
100
|
+
error_paths = [_mdit.element.code_span(error.json_path) for error in self.causes]
|
|
101
|
+
error_paths_str = self._join_list(error_paths)
|
|
102
|
+
count_errors = len(error_paths)
|
|
103
|
+
problem = _mdit.inline_container(
|
|
104
|
+
"Found ",
|
|
105
|
+
"an error " if count_errors == 1 else f"{count_errors} errors ",
|
|
106
|
+
"in the data at ",
|
|
107
|
+
error_paths_str,
|
|
108
|
+
"."
|
|
109
|
+
)
|
|
110
|
+
return problem
|
|
111
|
+
|
|
112
|
+
def _generate_error_report(
|
|
113
|
+
self,
|
|
114
|
+
error: _jsonschema.exceptions.ValidationError,
|
|
115
|
+
) -> _mdit.Document:
|
|
116
|
+
problem = self._parse_error_message(error)
|
|
117
|
+
short_ver_fieldlist_items = [
|
|
118
|
+
_mdit.element.field_list_item("Problem", problem),
|
|
119
|
+
_mdit.element.field_list_item(
|
|
120
|
+
"Validator Path", _mdit.element.code_span(self._create_path(error.absolute_schema_path))
|
|
121
|
+
),
|
|
122
|
+
]
|
|
123
|
+
full_ver_items = [
|
|
124
|
+
_mdit.inline_container(problem),
|
|
125
|
+
self._make_yaml_code_admo(
|
|
126
|
+
admo_type="error",
|
|
127
|
+
title="Validator",
|
|
128
|
+
title_details=str(error.validator),
|
|
129
|
+
content=error.validator_value,
|
|
130
|
+
),
|
|
131
|
+
self._make_yaml_code_admo(
|
|
132
|
+
admo_type="error",
|
|
133
|
+
title="Schema",
|
|
134
|
+
title_details=self._create_path(error.absolute_schema_path),
|
|
135
|
+
content=error.schema,
|
|
136
|
+
),
|
|
137
|
+
self._make_yaml_code_admo(
|
|
138
|
+
admo_type="error",
|
|
139
|
+
title="Instance",
|
|
140
|
+
title_details=self._create_path(error.absolute_path),
|
|
141
|
+
content=error.instance,
|
|
142
|
+
),
|
|
143
|
+
]
|
|
144
|
+
section = {}
|
|
145
|
+
if error.context:
|
|
146
|
+
context_paths = []
|
|
147
|
+
for idx, sub_error in enumerate(sorted(error.context, key=lambda x: len(x.context))):
|
|
148
|
+
section[idx] = self._generate_error_report(sub_error)
|
|
149
|
+
context_paths.append(_mdit.element.code_span(sub_error.json_path))
|
|
150
|
+
context_paths_joined = self._join_list(context_paths)
|
|
151
|
+
short_ver_fieldlist_items.insert(
|
|
152
|
+
1,
|
|
153
|
+
_mdit.element.field_list_item(
|
|
154
|
+
f"Context Path{'s' if len(context_paths) > 1 else ''}",
|
|
155
|
+
context_paths_joined
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
err = "an error" if len(context_paths) == 1 else f"{len(context_paths)} errors"
|
|
159
|
+
full_ver_items[0].append(
|
|
160
|
+
f" This was caused by {err} at {context_paths_joined}.",
|
|
161
|
+
)
|
|
162
|
+
doc = _mdit.document(
|
|
163
|
+
heading=_mdit.element.code_span(error.json_path),
|
|
164
|
+
body={
|
|
165
|
+
"short": (_mdit.element.field_list(short_ver_fieldlist_items), ("short", "console")),
|
|
166
|
+
"full": (_mdit.element.field_list(full_ver_items), "full"),
|
|
167
|
+
},
|
|
168
|
+
section=section,
|
|
169
|
+
)
|
|
170
|
+
return doc
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def _make_yaml_code_admo(admo_type: str, title: str, title_details: str, content: dict) -> _mdit.element.Admonition:
|
|
174
|
+
code_block = _mdit.element.code_block(
|
|
175
|
+
content=_write.to_yaml_string(content, end_of_file_newline=False),
|
|
176
|
+
language="yaml",
|
|
177
|
+
)
|
|
178
|
+
admo = _mdit.element.admonition(
|
|
179
|
+
type=admo_type,
|
|
180
|
+
title=_mdit.inline_container(f"**{title}**: ", _mdit.element.code_span(title_details)),
|
|
181
|
+
content=code_block,
|
|
182
|
+
dropdown=True,
|
|
183
|
+
)
|
|
184
|
+
return admo
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _parse_error_message(error: _jsonschema.exceptions.ValidationError) -> str:
|
|
188
|
+
instance_str = str(error.instance)
|
|
189
|
+
if error.message.startswith(instance_str):
|
|
190
|
+
msg = error.message.removeprefix(str(error.instance)).strip()
|
|
191
|
+
problem = f"Data {msg}"
|
|
192
|
+
else:
|
|
193
|
+
problem = error.message
|
|
194
|
+
return f"{problem.removeprefix(".")}."
|
|
195
|
+
|
|
196
|
+
@staticmethod
|
|
197
|
+
def _create_path(path):
|
|
198
|
+
return "$." + ".".join(str(path_component) for path_component in path)
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def _join_list(
|
|
202
|
+
items: list,
|
|
203
|
+
sep: Stringable = ", ",
|
|
204
|
+
sep_last: Stringable = ", and ",
|
|
205
|
+
sep_pair: Stringable = " and ",
|
|
206
|
+
) -> Stringable:
|
|
207
|
+
if len(items) == 1:
|
|
208
|
+
return items[0]
|
|
209
|
+
elif len(items) == 2:
|
|
210
|
+
return _mdit.inline_container(items[0], sep_pair, items[1])
|
|
211
|
+
container = []
|
|
212
|
+
for item in items[:-1]:
|
|
213
|
+
container.extend([item, sep])
|
|
214
|
+
container.pop()
|
|
215
|
+
container.extend([sep_last, items[-1]])
|
|
216
|
+
return _mdit.inline_container(*container)
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"""PySerials base Exception class."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
from typing import Literal as _Literal
|
|
5
|
-
import ansi_sgr as _sgr
|
|
6
|
-
from markitup import html as _html, doc as _doc
|
|
7
|
-
from exceptionman import ReporterException as _ReporterException
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
_ANSI_HEADING_STYLE = {
|
|
11
|
-
1: _sgr.style(text_styles="bold", background_color="red"),
|
|
12
|
-
2: _sgr.style(text_styles="bold", background_color="yellow"),
|
|
13
|
-
3: _sgr.style(text_styles="bold", background_color="green"),
|
|
14
|
-
4: _sgr.style(text_styles="bold", background_color="blue"),
|
|
15
|
-
5: _sgr.style(text_styles="bold", background_color="magenta"),
|
|
16
|
-
6: _sgr.style(text_styles="bold", background_color="cyan"),
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class PySerialsException(_ReporterException):
|
|
21
|
-
"""Base class for all exceptions raised by PySerials."""
|
|
22
|
-
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
message: str,
|
|
26
|
-
description: str | None = None,
|
|
27
|
-
message_html: str | _html.Element | None = None,
|
|
28
|
-
description_html: str | _html.Element | None = None,
|
|
29
|
-
report_heading: str = "PySerials Error Report",
|
|
30
|
-
):
|
|
31
|
-
super().__init__(
|
|
32
|
-
message=message,
|
|
33
|
-
description=description,
|
|
34
|
-
message_html=message_html,
|
|
35
|
-
description_html=description_html,
|
|
36
|
-
report_heading=report_heading,
|
|
37
|
-
)
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def ansi_heading(section: list[int], title: str) -> str:
|
|
42
|
-
"""Get an ANSI SGR formatted heading."""
|
|
43
|
-
sec = ".".join(str(n) for n in section)
|
|
44
|
-
sec_level = min(len(section), 6)
|
|
45
|
-
heading = f" {sec}. {title} "
|
|
46
|
-
return _sgr.format(heading, control_sequence=_ANSI_HEADING_STYLE[sec_level])
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def ansi_bold(text: str) -> str:
|
|
50
|
-
return _sgr.format(text, control_sequence=_sgr.style(text_styles="bold"))
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def format_code(code: str) -> tuple[str, str]:
|
|
54
|
-
console = _sgr.format(
|
|
55
|
-
code, control_sequence=_sgr.style(text_color=(220, 220, 220), background_color=(20, 20, 20))
|
|
56
|
-
)
|
|
57
|
-
html = str(_html.elem.code(code))
|
|
58
|
-
return console, html
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
"""Exceptions raised by `pyserials.read` module."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
from typing import Literal as _Literal
|
|
5
|
-
from pathlib import Path as _Path
|
|
6
|
-
|
|
7
|
-
import ruamel.yaml as _yaml
|
|
8
|
-
import json as _json
|
|
9
|
-
|
|
10
|
-
from markitup import html as _html, md as _md
|
|
11
|
-
from tomlkit.exceptions import TOMLKitError as _TOMLKitError
|
|
12
|
-
|
|
13
|
-
from pyserials.exception._base import PySerialsException as _PySerialsException
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class PySerialsReadException(_PySerialsException):
|
|
17
|
-
"""Base class for all exceptions raised by `pyserials.read` module.
|
|
18
|
-
|
|
19
|
-
Attributes
|
|
20
|
-
----------
|
|
21
|
-
source_type : {"file", "string"}
|
|
22
|
-
Type of source from which data was read.
|
|
23
|
-
data_type : {"json", "yaml", "toml"} or None
|
|
24
|
-
Type of input data, if known.
|
|
25
|
-
filepath : pathlib.Path or None
|
|
26
|
-
Path to the input datafile, if data was read from a file.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(
|
|
30
|
-
self,
|
|
31
|
-
source_type: _Literal["file", "string"],
|
|
32
|
-
description: str,
|
|
33
|
-
description_html: str | _html.Element | None = None,
|
|
34
|
-
data_type: _Literal["json", "yaml", "toml"] | None = None,
|
|
35
|
-
filepath: _Path | None = None,
|
|
36
|
-
):
|
|
37
|
-
if source_type == "string":
|
|
38
|
-
source = source_html = "string"
|
|
39
|
-
else:
|
|
40
|
-
source = f"file at '{filepath}'"
|
|
41
|
-
source_html = f"file at <code>{filepath}</code>"
|
|
42
|
-
data_ = f"{data_type.upper()} data" if data_type else "data"
|
|
43
|
-
message_template = f"Failed to read {data_} from input {{source}}."
|
|
44
|
-
super().__init__(
|
|
45
|
-
message=message_template.format(source=source),
|
|
46
|
-
message_html=message_template.format(source=source_html),
|
|
47
|
-
description=description,
|
|
48
|
-
description_html=description_html,
|
|
49
|
-
report_heading="PySerials Read Error Report",
|
|
50
|
-
)
|
|
51
|
-
self.source_type: _Literal["file", "string"] = source_type
|
|
52
|
-
self.data_type: _Literal["json", "yaml", "toml"] | None = data_type
|
|
53
|
-
self.filepath: _Path | None = filepath
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class PySerialsEmptyStringError(PySerialsReadException):
|
|
58
|
-
"""Exception raised when a string to be read is empty."""
|
|
59
|
-
|
|
60
|
-
def __init__(self, data_type: _Literal["json", "yaml", "toml"]):
|
|
61
|
-
description = f"The string is empty."
|
|
62
|
-
super().__init__(description=description, source_type="string", data_type=data_type)
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
class PySerialsInvalidFileExtensionError(PySerialsReadException):
|
|
67
|
-
"""Exception raised when a file to be read has an unrecognized extension."""
|
|
68
|
-
|
|
69
|
-
def __init__(self, filepath: _Path):
|
|
70
|
-
description = (
|
|
71
|
-
f"The file extension must be one of 'json', 'yaml', 'yml', or '.toml', "
|
|
72
|
-
f"but got '{filepath.suffix.removeprefix('.')}'. "
|
|
73
|
-
"Please provide the extension explicitly, or rename the file to have a valid extension."
|
|
74
|
-
)
|
|
75
|
-
super().__init__(description=description, source_type="file", filepath=filepath)
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class PySerialsMissingFileError(PySerialsReadException):
|
|
80
|
-
"""Exception raised when a file to be read does not exist."""
|
|
81
|
-
|
|
82
|
-
def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
|
|
83
|
-
description = f"The file does not exist."
|
|
84
|
-
super().__init__(description=description, source_type="file", data_type=data_type, filepath=filepath)
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
class PySerialsEmptyFileError(PySerialsReadException):
|
|
89
|
-
"""Exception raised when a file to be read is empty."""
|
|
90
|
-
|
|
91
|
-
def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
|
|
92
|
-
description = f"The file is empty."
|
|
93
|
-
super().__init__(description=description, source_type="file", data_type=data_type, filepath=filepath)
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class PySerialsInvalidDataError(PySerialsReadException):
|
|
98
|
-
"""Exception raised when the data is invalid.
|
|
99
|
-
|
|
100
|
-
Attributes
|
|
101
|
-
----------
|
|
102
|
-
data : str
|
|
103
|
-
The input data that was supposed to be read.
|
|
104
|
-
"""
|
|
105
|
-
|
|
106
|
-
def __init__(
|
|
107
|
-
self,
|
|
108
|
-
source_type: _Literal["file", "string"],
|
|
109
|
-
data_type: _Literal["json", "yaml", "toml"],
|
|
110
|
-
data: str,
|
|
111
|
-
cause: Exception,
|
|
112
|
-
filepath: _Path | None = None,
|
|
113
|
-
):
|
|
114
|
-
self.data = data
|
|
115
|
-
self.cause = cause
|
|
116
|
-
self.problem: str = str(cause)
|
|
117
|
-
self.problem_line: int | None = None
|
|
118
|
-
self.problem_column: int | None = None
|
|
119
|
-
self.problem_data_type: str | None = None
|
|
120
|
-
self.context: str | None = None
|
|
121
|
-
self.context_line: int | None = None
|
|
122
|
-
self.context_column: int | None = None
|
|
123
|
-
self.context_data_type: str | None = None
|
|
124
|
-
|
|
125
|
-
if isinstance(cause, _yaml.YAMLError):
|
|
126
|
-
self.problem_line = cause.problem_mark.line + 1
|
|
127
|
-
self.problem_column = cause.problem_mark.column + 1
|
|
128
|
-
self.problem_data_type = cause.problem_mark.name
|
|
129
|
-
self.problem = cause.problem.strip()
|
|
130
|
-
if cause.context:
|
|
131
|
-
self.context = cause.context.strip()
|
|
132
|
-
self.context_line = cause.context_mark.line + 1
|
|
133
|
-
self.context_column = cause.context_mark.column + 1
|
|
134
|
-
self.context_data_type = cause.context_mark.name
|
|
135
|
-
elif isinstance(cause, _json.JSONDecodeError):
|
|
136
|
-
self.problem = cause.msg
|
|
137
|
-
self.problem_line = cause.lineno
|
|
138
|
-
self.problem_column = cause.colno
|
|
139
|
-
elif isinstance(cause, _TOMLKitError):
|
|
140
|
-
self.problem_line = cause.line
|
|
141
|
-
self.problem_column = cause.col
|
|
142
|
-
self.problem = cause.args[0].removesuffix(f" at line {self.problem_line} col {self.problem_column}")
|
|
143
|
-
self.problem = self.problem.strip().capitalize().removesuffix(".")
|
|
144
|
-
description = "The data is not valid"
|
|
145
|
-
if self.problem_line:
|
|
146
|
-
description += f" at line {self.problem_line}, column {self.problem_column}"
|
|
147
|
-
description += f": {self.problem}."
|
|
148
|
-
super().__init__(description=description, source_type=source_type, data_type=data_type, filepath=filepath)
|
|
149
|
-
return
|
|
150
|
-
|
|
151
|
-
def _report_content(self, mode: _Literal["full", "short"], md: bool) -> _html.elem.Ul:
|
|
152
|
-
|
|
153
|
-
def make_tabel(problem, line, column, data_type):
|
|
154
|
-
rows = [
|
|
155
|
-
[title, value] for title, value in [
|
|
156
|
-
["Description", problem],
|
|
157
|
-
["Line Number", line],
|
|
158
|
-
["Column Number", column],
|
|
159
|
-
["Data Type", data_type],
|
|
160
|
-
] if value is not None
|
|
161
|
-
]
|
|
162
|
-
return _html.elem.table_from_rows(rows_body=rows)
|
|
163
|
-
|
|
164
|
-
content_list = [
|
|
165
|
-
_html.elem.details(
|
|
166
|
-
[
|
|
167
|
-
_html.elem.summary("❌ Problem"),
|
|
168
|
-
make_tabel(self.problem, self.problem_line, self.problem_column, self.problem_data_type)
|
|
169
|
-
]
|
|
170
|
-
),
|
|
171
|
-
]
|
|
172
|
-
if self.context:
|
|
173
|
-
content_list.append(
|
|
174
|
-
_html.elem.details(
|
|
175
|
-
[
|
|
176
|
-
_html.elem.summary("🔍 Context"),
|
|
177
|
-
make_tabel(self.context, self.context_line, self.context_column, self.context_data_type)
|
|
178
|
-
]
|
|
179
|
-
),
|
|
180
|
-
)
|
|
181
|
-
data_lines = self.data.splitlines()
|
|
182
|
-
if self.problem_line:
|
|
183
|
-
line_idx = self.problem_line - 1
|
|
184
|
-
problem_line = data_lines[line_idx]
|
|
185
|
-
if md:
|
|
186
|
-
problem_line = f"- {problem_line}"
|
|
187
|
-
else:
|
|
188
|
-
if self.problem_column:
|
|
189
|
-
col_idx = self.problem_column - 1
|
|
190
|
-
prob_col = problem_line[col_idx]
|
|
191
|
-
prob_col_highlight = _html.elem.span(prob_col, {"class": "highlight-char"})
|
|
192
|
-
problem_line = f"{problem_line[:col_idx]}{prob_col_highlight}{problem_line[col_idx+1:]}"
|
|
193
|
-
problem_line = str(_html.elem.span(problem_line, {"class": "highlight-line"}))
|
|
194
|
-
data_lines[line_idx] = problem_line
|
|
195
|
-
if mode == "short":
|
|
196
|
-
if self.problem_line is not None:
|
|
197
|
-
problem_line_idx = self.problem_line - 1
|
|
198
|
-
start_line = max(0, problem_line_idx)
|
|
199
|
-
end_line = min(len(data_lines), problem_line_idx + 1)
|
|
200
|
-
if self.context_line is not None:
|
|
201
|
-
context_line_idx = self.context_line - 1
|
|
202
|
-
start_line = max(0, context_line_idx, problem_line_idx)
|
|
203
|
-
end_line = min(len(data_lines), context_line_idx + 1, problem_line_idx + 1)
|
|
204
|
-
data_lines = data_lines[start_line:end_line]
|
|
205
|
-
data = "\n".join(data_lines)
|
|
206
|
-
if md:
|
|
207
|
-
code_block = _md.elem.code_fence(data, info="diff")
|
|
208
|
-
else:
|
|
209
|
-
code_block = _html.elem.pre(_html.elem.code(data, {"class": f"language-{self.data_type}"}))
|
|
210
|
-
content_list.append(_html.elem.details([_html.elem.summary("📄 Data"), code_block]))
|
|
211
|
-
return _html.elem.ul([_html.elem.li(content) for content in content_list])
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
"""Exceptions raised by `pyserials.validate` module."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
from typing import Any as _Any, Literal as _Literal
|
|
5
|
-
|
|
6
|
-
import jsonschema as _jsonschema
|
|
7
|
-
from markitup.html import elem as _html
|
|
8
|
-
from markitup.md import elem as _md
|
|
9
|
-
from pyserials import write as _write
|
|
10
|
-
from pyserials.exception import _base
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class PySerialsValidateException(_base.PySerialsException):
|
|
14
|
-
"""Base class for all exceptions raised by `pyserials.validate` module.
|
|
15
|
-
|
|
16
|
-
Attributes
|
|
17
|
-
----------
|
|
18
|
-
data : dict | list | str | int | float | bool
|
|
19
|
-
The data that failed validation.
|
|
20
|
-
schema : dict
|
|
21
|
-
The schema that the data failed to validate against.
|
|
22
|
-
validator : Any
|
|
23
|
-
The validator that was used to validate the data against the schema.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
def __init__(
|
|
27
|
-
self,
|
|
28
|
-
description: str,
|
|
29
|
-
data: dict | list | str | int | float | bool,
|
|
30
|
-
schema: dict,
|
|
31
|
-
validator: _Any,
|
|
32
|
-
registry: _Any = None,
|
|
33
|
-
description_html: str | _html.Element | None = None,
|
|
34
|
-
):
|
|
35
|
-
validator_name = validator.__class__.__name__
|
|
36
|
-
message_template = "Failed to validate data against schema using validator {validator_name}."
|
|
37
|
-
validator_name_console, validator_name_html = _base.format_code(validator_name)
|
|
38
|
-
super().__init__(
|
|
39
|
-
message=message_template.format(validator_name=validator_name_console),
|
|
40
|
-
message_html=message_template.format(validator_name=validator_name_html),
|
|
41
|
-
description=description,
|
|
42
|
-
description_html=description_html,
|
|
43
|
-
report_heading="PySerials Schema Validation Error Report",
|
|
44
|
-
)
|
|
45
|
-
self.data = data
|
|
46
|
-
self.schema = schema
|
|
47
|
-
self.validator = validator
|
|
48
|
-
self.registry = registry
|
|
49
|
-
return
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class PySerialsInvalidJsonSchemaError(PySerialsValidateException):
|
|
53
|
-
"""Exception raised when data validation fails due to the schema being invalid."""
|
|
54
|
-
|
|
55
|
-
def __init__(
|
|
56
|
-
self,
|
|
57
|
-
data: dict | list | str | int | float | bool,
|
|
58
|
-
schema: dict,
|
|
59
|
-
validator: _Any,
|
|
60
|
-
registry: _Any = None,
|
|
61
|
-
):
|
|
62
|
-
super().__init__(
|
|
63
|
-
description="The schema is invalid.",
|
|
64
|
-
data=data,
|
|
65
|
-
schema=schema,
|
|
66
|
-
validator=validator,
|
|
67
|
-
registry=registry,
|
|
68
|
-
)
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class PySerialsJsonSchemaValidationError(PySerialsValidateException):
|
|
73
|
-
"""Exception raised when data validation fails due to the data being invalid against the schema."""
|
|
74
|
-
|
|
75
|
-
def __init__(
|
|
76
|
-
self,
|
|
77
|
-
causes: list[_jsonschema.exceptions.ValidationError],
|
|
78
|
-
data: dict | list | str | int | float | bool,
|
|
79
|
-
schema: dict,
|
|
80
|
-
validator: _Any,
|
|
81
|
-
registry: _Any = None,
|
|
82
|
-
):
|
|
83
|
-
self.causes = causes
|
|
84
|
-
description, description_html = self._parse_errors()
|
|
85
|
-
super().__init__(
|
|
86
|
-
description=description,
|
|
87
|
-
description_html=description_html,
|
|
88
|
-
data=data,
|
|
89
|
-
schema=schema,
|
|
90
|
-
validator=validator,
|
|
91
|
-
registry=registry,
|
|
92
|
-
)
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
def _report_content(
|
|
96
|
-
self, mode: _Literal["full", "short"], md: bool
|
|
97
|
-
) -> list[str | _html.Element] | str | _html.Element | None:
|
|
98
|
-
html_details = []
|
|
99
|
-
for idx, error in enumerate(self.causes):
|
|
100
|
-
section_html = self._report_error(error, section=[idx + 1], mode=mode, md=md)
|
|
101
|
-
html_details.append(section_html)
|
|
102
|
-
return _html.ul([_html.li(detail) for detail in html_details])
|
|
103
|
-
|
|
104
|
-
def _parse_errors(self) -> tuple[str, str]:
|
|
105
|
-
count_errors = len(self.causes)
|
|
106
|
-
errors = "an error" if count_errors == 1 else f"{count_errors} errors"
|
|
107
|
-
intro = intro_html = f"Found {errors} in the data at "
|
|
108
|
-
reports = []
|
|
109
|
-
errors_loc = []
|
|
110
|
-
errors_loc_html = []
|
|
111
|
-
for idx, error in enumerate(self.causes):
|
|
112
|
-
error_loc, error_loc_html = _base.format_code(error.json_path)
|
|
113
|
-
errors_loc.append(error_loc)
|
|
114
|
-
errors_loc_html.append(error_loc_html)
|
|
115
|
-
section_console = self._parse_error(error, section=[idx + 1])
|
|
116
|
-
reports.extend(section_console)
|
|
117
|
-
reports_str = "\n".join(reports)
|
|
118
|
-
if len(errors_loc) == 1:
|
|
119
|
-
errors_loc_str = errors_loc[0]
|
|
120
|
-
errors_loc_html_str = errors_loc_html[0]
|
|
121
|
-
elif len(errors_loc) == 2:
|
|
122
|
-
errors_loc_str = " and ".join(errors_loc)
|
|
123
|
-
errors_loc_html_str = " and ".join(errors_loc_html)
|
|
124
|
-
else:
|
|
125
|
-
errors_loc_str = ", ".join(errors_loc[:-1]) + " and " + errors_loc[-1]
|
|
126
|
-
errors_loc_html_str = ", ".join(errors_loc_html[:-1]) + " and " + errors_loc_html[-1]
|
|
127
|
-
intro += f"{errors_loc_str}:\n\n{reports_str}"
|
|
128
|
-
intro_html += f"{errors_loc_html_str}."
|
|
129
|
-
return intro, intro_html
|
|
130
|
-
|
|
131
|
-
def _parse_error(
|
|
132
|
-
self,
|
|
133
|
-
error: _jsonschema.exceptions.ValidationError,
|
|
134
|
-
section: list[int]
|
|
135
|
-
) -> list:
|
|
136
|
-
schema_path = self._create_path(error.absolute_schema_path)
|
|
137
|
-
title, _ = _base.format_code(error.json_path)
|
|
138
|
-
console = [
|
|
139
|
-
_base.ansi_heading(section, title),
|
|
140
|
-
f"- {_base.ansi_bold('Problem')}: {self._parse_error_message(error)}",
|
|
141
|
-
f"- {_base.ansi_bold("Validator Path")}: {schema_path}",
|
|
142
|
-
]
|
|
143
|
-
if error.context:
|
|
144
|
-
console.append(f"- {_base.ansi_bold("Context")}:")
|
|
145
|
-
for idx, sub_error in enumerate(sorted(error.context, key=lambda x: len(x.context))):
|
|
146
|
-
sub_console = self._parse_error(
|
|
147
|
-
sub_error,
|
|
148
|
-
section=section + [idx+1]
|
|
149
|
-
)
|
|
150
|
-
console.extend(self._indent(sub_console, 2))
|
|
151
|
-
return console
|
|
152
|
-
|
|
153
|
-
def _report_error(
|
|
154
|
-
self,
|
|
155
|
-
error: _jsonschema.exceptions.ValidationError,
|
|
156
|
-
section: list[int],
|
|
157
|
-
mode: _Literal["full", "short"],
|
|
158
|
-
md: bool
|
|
159
|
-
) -> _html.Details:
|
|
160
|
-
details = [
|
|
161
|
-
_html.p(f"{_html.b("Problem")}: {self._parse_error_message(error)}"),
|
|
162
|
-
self._create_validator_details(error, mode=mode, md=md),
|
|
163
|
-
self._create_schema_details(error, mode=mode, md=md),
|
|
164
|
-
self._create_instance_details(error, mode=mode, md=md),
|
|
165
|
-
]
|
|
166
|
-
if error.context:
|
|
167
|
-
contexts = []
|
|
168
|
-
for idx, sub_error in enumerate(sorted(error.context, key=lambda x: len(x.context))):
|
|
169
|
-
sub_html = self._report_error(
|
|
170
|
-
sub_error,
|
|
171
|
-
section=section + [idx+1],
|
|
172
|
-
mode=mode,
|
|
173
|
-
md=md,
|
|
174
|
-
)
|
|
175
|
-
contexts.append(sub_html)
|
|
176
|
-
context_list = _html.ul([_html.li(context) for context in contexts])
|
|
177
|
-
details.append(f"{_html.b("Context")}: {context_list}")
|
|
178
|
-
_, title = _base.format_code(error.json_path)
|
|
179
|
-
html = _html.details(
|
|
180
|
-
[_html.summary(title), _html.ul([_html.li(elem) for elem in details])]
|
|
181
|
-
)
|
|
182
|
-
return html
|
|
183
|
-
|
|
184
|
-
def _create_validator_details(self, error: _jsonschema.exceptions.ValidationError, mode: _Literal["full", "short"], md: bool) -> _html.Details:
|
|
185
|
-
summary = self._create_details_summary("Validator", str(error.validator))
|
|
186
|
-
return summary if mode == "short" else self._make_details(
|
|
187
|
-
content=error.validator_value,
|
|
188
|
-
summary=summary,
|
|
189
|
-
md=md,
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
def _create_schema_details(self, error: _jsonschema.exceptions.ValidationError, mode: _Literal["full", "short"], md: bool) -> _html.Details:
|
|
193
|
-
summary = self._create_details_summary("Schema", self._create_path(error.absolute_schema_path))
|
|
194
|
-
return summary if mode == "short" else self._make_details(
|
|
195
|
-
content=error.schema,
|
|
196
|
-
summary=summary,
|
|
197
|
-
md=md,
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
def _create_instance_details(self, error: _jsonschema.exceptions.ValidationError, mode: _Literal["full", "short"], md: bool) -> _html.Details:
|
|
201
|
-
summary = self._create_details_summary("Instance", self._create_path(error.absolute_path))
|
|
202
|
-
return summary if mode == "short" else self._make_details(
|
|
203
|
-
content=error.instance,
|
|
204
|
-
summary=summary,
|
|
205
|
-
md=md,
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
@staticmethod
|
|
209
|
-
def _make_details(content: dict, summary: str, md: bool) -> _html.Details:
|
|
210
|
-
yaml = _write.to_yaml_string(content, end_of_file_newline=False)
|
|
211
|
-
details_summary = _html.summary(summary)
|
|
212
|
-
details_content = _html.pre(
|
|
213
|
-
_html.code(yaml, {"class": "language-yaml"})
|
|
214
|
-
) if not md else _md.code_fence(yaml, info="yaml")
|
|
215
|
-
return _html.details([details_summary, details_content])
|
|
216
|
-
|
|
217
|
-
def _create_title(self, error: _jsonschema.exceptions.ValidationError) -> tuple[str, str]:
|
|
218
|
-
problem = self._parse_error_message(error)
|
|
219
|
-
title_shell = f"'{error.json_path}': {problem}"
|
|
220
|
-
title_html = f"{_html.code(error.json_path)}: {problem}"
|
|
221
|
-
return title_shell, title_html
|
|
222
|
-
|
|
223
|
-
@staticmethod
|
|
224
|
-
def _parse_error_message(error: _jsonschema.exceptions.ValidationError) -> str:
|
|
225
|
-
instance_str = str(error.instance)
|
|
226
|
-
if error.message.startswith(instance_str):
|
|
227
|
-
msg = error.message.removeprefix(str(error.instance)).strip()
|
|
228
|
-
problem = f"Data {msg}"
|
|
229
|
-
else:
|
|
230
|
-
problem = error.message
|
|
231
|
-
return problem
|
|
232
|
-
|
|
233
|
-
@staticmethod
|
|
234
|
-
def _indent(text: list, indent: int = 2) -> list:
|
|
235
|
-
return [f"{' ' * indent}{line}" for line in text]
|
|
236
|
-
|
|
237
|
-
@staticmethod
|
|
238
|
-
def _create_path(path):
|
|
239
|
-
return "$." + ".".join(str(path_component) for path_component in path)
|
|
240
|
-
|
|
241
|
-
@staticmethod
|
|
242
|
-
def _create_details_summary(title: str, code: str) -> str:
|
|
243
|
-
return f"{_html.b(title)}: {_html.code(code)}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|