PySerials 0.0.0.dev5__tar.gz → 0.0.0.dev7__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.
Files changed (26) hide show
  1. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/PKG-INFO +3 -4
  2. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/pyproject.toml +3 -4
  3. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/PySerials.egg-info/PKG-INFO +3 -4
  4. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/PySerials.egg-info/requires.txt +2 -3
  5. pyserials-0.0.0.dev7/src/pyserials/exception/_base.py +26 -0
  6. pyserials-0.0.0.dev7/src/pyserials/exception/read.py +241 -0
  7. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/exception/update.py +37 -38
  8. pyserials-0.0.0.dev7/src/pyserials/exception/validate.py +216 -0
  9. pyserials-0.0.0.dev5/src/pyserials/exception/_base.py +0 -58
  10. pyserials-0.0.0.dev5/src/pyserials/exception/read.py +0 -211
  11. pyserials-0.0.0.dev5/src/pyserials/exception/validate.py +0 -243
  12. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/README.md +0 -0
  13. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/setup.cfg +0 -0
  14. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/PySerials.egg-info/SOURCES.txt +0 -0
  15. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/PySerials.egg-info/dependency_links.txt +0 -0
  16. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/PySerials.egg-info/not-zip-safe +0 -0
  17. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/PySerials.egg-info/top_level.txt +0 -0
  18. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/__init__.py +0 -0
  19. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/compare.py +0 -0
  20. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/exception/__init__.py +0 -0
  21. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/format.py +0 -0
  22. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/nested_dict.py +0 -0
  23. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/read.py +0 -0
  24. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/update.py +0 -0
  25. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/validate.py +0 -0
  26. {pyserials-0.0.0.dev5 → pyserials-0.0.0.dev7}/src/pyserials/write.py +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySerials
3
- Version: 0.0.0.dev5
3
+ Version: 0.0.0.dev7
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: markitup
10
- Requires-Dist: ansi-sgr
11
- Requires-Dist: ExceptionMan
9
+ Requires-Dist: MDit==0.0.0.dev3
10
+ Requires-Dist: ExceptionMan==0.0.0.dev4
12
11
  Requires-Dist: jsonpath-ng<2,>=1.6.1
@@ -17,16 +17,15 @@ namespaces = true
17
17
  # ----------------------------------------- Project Metadata -------------------------------------
18
18
  #
19
19
  [project]
20
- version = "0.0.0.dev5"
20
+ version = "0.0.0.dev7"
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
- "markitup",
28
- "ansi-sgr",
29
- "ExceptionMan",
27
+ "MDit == 0.0.0.dev3",
28
+ "ExceptionMan == 0.0.0.dev4",
30
29
  "jsonpath-ng >= 1.6.1, < 2",
31
30
  ]
32
31
  requires-python = ">=3.10"
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySerials
3
- Version: 0.0.0.dev5
3
+ Version: 0.0.0.dev7
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: markitup
10
- Requires-Dist: ansi-sgr
11
- Requires-Dist: ExceptionMan
9
+ Requires-Dist: MDit==0.0.0.dev3
10
+ Requires-Dist: ExceptionMan==0.0.0.dev4
12
11
  Requires-Dist: jsonpath-ng<2,>=1.6.1
@@ -2,7 +2,6 @@ jsonschema<5,>=4.21.0
2
2
  ruamel.yaml<0.18,>=0.17.32
3
3
  ruamel.yaml.string<1,>=0.1.1
4
4
  tomlkit<0.12,>=0.11.8
5
- markitup
6
- ansi-sgr
7
- ExceptionMan
5
+ MDit==0.0.0.dev3
6
+ ExceptionMan==0.0.0.dev4
8
7
  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,241 @@
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
+ separator=" ",
45
+ )
46
+ report = _mdit.document(
47
+ heading="Data Read Error",
48
+ body={
49
+ "intro": intro,
50
+ "problem": problem,
51
+ },
52
+ section=section,
53
+ )
54
+ super().__init__(report)
55
+ self.source_type: _Literal["file", "string"] = source_type
56
+ self.data_type: _Literal["json", "yaml", "toml"] | None = data_type
57
+ self.filepath: _Path | None = filepath
58
+ return
59
+
60
+
61
+ class PySerialsEmptyStringError(PySerialsReadException):
62
+ """Exception raised when a string to be read is empty."""
63
+
64
+ def __init__(self, data_type: _Literal["json", "yaml", "toml"]):
65
+ problem = f"The string is empty."
66
+ super().__init__(problem=problem, source_type="string", data_type=data_type)
67
+ return
68
+
69
+
70
+ class PySerialsInvalidFileExtensionError(PySerialsReadException):
71
+ """Exception raised when a file to be read has an unrecognized extension."""
72
+
73
+ def __init__(self, filepath: _Path):
74
+ problem = _mdit.inline_container(
75
+ "The file extension must be one of ",
76
+ _mdit.element.code_span('json'),
77
+ ", ",
78
+ _mdit.element.code_span('yaml'),
79
+ ", ",
80
+ _mdit.element.code_span('yml'),
81
+ ", or ",
82
+ _mdit.element.code_span('.toml'),
83
+ ", but got ",
84
+ _mdit.element.code_span(str(filepath.suffix.removeprefix('.'))),
85
+ ". Please provide the extension explicitly, or rename the file to have a valid extension."
86
+ )
87
+ super().__init__(problem=problem, source_type="file", filepath=filepath)
88
+ return
89
+
90
+
91
+ class PySerialsMissingFileError(PySerialsReadException):
92
+ """Exception raised when a file to be read does not exist."""
93
+
94
+ def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
95
+ problem = f"The file does not exist."
96
+ super().__init__(problem=problem, source_type="file", data_type=data_type, filepath=filepath)
97
+ return
98
+
99
+
100
+ class PySerialsEmptyFileError(PySerialsReadException):
101
+ """Exception raised when a file to be read is empty."""
102
+
103
+ def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
104
+ problem = f"The file is empty."
105
+ super().__init__(problem=problem, source_type="file", data_type=data_type, filepath=filepath)
106
+ return
107
+
108
+
109
+ class PySerialsInvalidDataError(PySerialsReadException):
110
+ """Exception raised when the data is invalid.
111
+
112
+ Attributes
113
+ ----------
114
+ data : str
115
+ The input data that was supposed to be read.
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ source_type: _Literal["file", "string"],
121
+ data_type: _Literal["json", "yaml", "toml"],
122
+ data: str,
123
+ cause: Exception,
124
+ filepath: _Path | None = None,
125
+ ):
126
+ self.data = data
127
+ self.cause = cause
128
+ self.problem: str = str(cause)
129
+ self.problem_line: int | None = None
130
+ self.problem_column: int | None = None
131
+ self.problem_data_type: str | None = None
132
+ self.context: str | None = None
133
+ self.context_line: int | None = None
134
+ self.context_column: int | None = None
135
+ self.context_data_type: str | None = None
136
+ self.data_type = data_type
137
+
138
+ if isinstance(cause, _yaml.YAMLError):
139
+ self.problem_line = cause.problem_mark.line + 1
140
+ self.problem_column = cause.problem_mark.column + 1
141
+ self.problem_data_type = cause.problem_mark.name.removeprefix("<").removesuffix(">")
142
+ self.problem = cause.problem.strip()
143
+ if cause.context:
144
+ self.context = cause.context.strip()
145
+ self.context_line = cause.context_mark.line + 1
146
+ self.context_column = cause.context_mark.column + 1
147
+ self.context_data_type = cause.context_mark.name.removeprefix("<").removesuffix(">")
148
+ elif isinstance(cause, _json.JSONDecodeError):
149
+ self.problem = cause.msg
150
+ self.problem_line = cause.lineno
151
+ self.problem_column = cause.colno
152
+ elif isinstance(cause, _TOMLKitError):
153
+ self.problem_line = cause.line
154
+ self.problem_column = cause.col
155
+ self.problem = cause.args[0].removesuffix(f" at line {self.problem_line} col {self.problem_column}")
156
+ self.problem = self.problem.strip().capitalize().removesuffix(".")
157
+ description = ["The data is not valid"]
158
+ if self.problem_line:
159
+ description.extend(
160
+ [
161
+ " at line ",
162
+ _mdit.element.code_span(str(self.problem_line)),
163
+ ", column ",
164
+ _mdit.element.code_span(str(self.problem_column)),
165
+ ]
166
+ )
167
+ description.append(f": {self.problem}.")
168
+ super().__init__(
169
+ problem=description,
170
+ source_type=source_type,
171
+ section=self._report_content(),
172
+ data_type=data_type,
173
+ filepath=filepath
174
+ )
175
+ return
176
+
177
+ def _report_content(self) -> dict:
178
+
179
+ def make_table(problem, line, column, data_type):
180
+ items = [
181
+ _mdit.element.field_list_item(title=title, description=value) for title, value in [
182
+ ["Description", problem],
183
+ ["Line Number", line],
184
+ ["Column Number", column],
185
+ ["Data Type", data_type],
186
+ ] if value is not None
187
+ ]
188
+ return _mdit.element.field_list(items)
189
+
190
+ content = {
191
+ "problem_details": _mdit.element.admonition(
192
+ make_table(self.problem, self.problem_line, self.problem_column, self.problem_data_type),
193
+ type="error",
194
+ title="Problem",
195
+ )
196
+ }
197
+ if self.context:
198
+ content["context_details"] = _mdit.element.admonition(
199
+ make_table(self.context, self.context_line, self.context_column, self.context_data_type),
200
+ type="note",
201
+ title="Context",
202
+ )
203
+
204
+ code_block_full = _mdit.element.code_block(
205
+ content=self.data,
206
+ language=self.data_type,
207
+ caption="Data",
208
+ line_num=True,
209
+ emphasize_lines=[line for line in (self.problem_line, self.context_line) if line],
210
+ degrade_to_diff=True,
211
+ )
212
+ content["data_full"] = (code_block_full, "full")
213
+
214
+ if not (self.problem_line or self.context_line):
215
+ code_block_short = _mdit.element.code_block(
216
+ content=self.data[:1000].strip() + "\n..." if len(self.data) > 1000 else self.data,
217
+ language=self.data_type,
218
+ caption="Data" if len(self.data) <= 1000 else "Data (truncated to first 1000 characters)",
219
+ line_num=True,
220
+ )
221
+ else:
222
+ if self.problem_line and self.context_line:
223
+ line_start = min(self.problem_line, self.context_line)
224
+ line_end = max(self.problem_line, self.context_line)
225
+ else:
226
+ line_start = self.problem_line or self.context_line
227
+ line_end = line_start
228
+ data_lines = self.data.splitlines()
229
+ selected_lines = data_lines[line_start - 1:line_end]
230
+ code_block_short = _mdit.element.code_block(
231
+ content="\n".join(selected_lines),
232
+ language=self.data_type,
233
+ caption="Data",
234
+ line_num=True,
235
+ line_num_start=line_start,
236
+ emphasize_lines=list({1, len(selected_lines)}),
237
+ )
238
+ content["data_short"] = (code_block_short, ("short", "console"))
239
+ container = _mdit.block_container(content)
240
+ doc = _mdit.document(heading="Error Details", body=container)
241
+ 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
- from markitup.html import elem as _html
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
- description: str,
30
- description_html: str | _html.Element | None = None,
29
+ problem,
30
+ section: dict | None = None,
31
31
  ):
32
- message_template = "Failed to update data at {path}."
33
- path_console, path_html = _base.format_code(path)
34
- super().__init__(
35
- message=message_template.format(path=path_console),
36
- message_html=message_template.format(path=path_html),
37
- description=description,
38
- description_html=description_html,
39
- report_heading="PySerials Update Error Report",
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
- type_data_console, type_data_html = _base.format_code(self.type_data.__name__)
70
- type_data_addon_console, type_data_addon_html = _base.format_code(self.type_data_addon.__name__)
71
- path_console, path_html = _base.format_code(path)
72
- kwargs_console, kwargs_html = (
73
- {"path": path, "type_data": type_data, "type_data_addon": type_data_addon}
74
- for path, type_data, type_data_addon in zip(
75
- (path_console, path_html),
76
- (type_data_console, type_data_html),
77
- (type_data_addon_console, type_data_addon_html),
78
- )
79
- )
80
- description_template = (
81
- "There was a duplicate in the addon dictionary; "
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
- path_invalid_console, path_invalid_html = _base.format_code(path_invalid.replace("'", ""))
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,
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={str(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[str(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.block_container(*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
+ code_block,
180
+ type=admo_type,
181
+ title=_mdit.inline_container(f"**{title}**: ", _mdit.element.code_span(title_details)),
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