PySerials 0.0.0.dev5__py3-none-any.whl → 0.0.0.dev6__py3-none-any.whl

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.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySerials
3
- Version: 0.0.0.dev5
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: markitup
9
+ Requires-Dist: mdit
10
10
  Requires-Dist: ansi-sgr
11
11
  Requires-Dist: ExceptionMan
12
12
  Requires-Dist: jsonpath-ng <2,>=1.6.1
@@ -7,11 +7,11 @@ pyserials/update.py,sha256=GfrFgc1a2qYqR8zs04cngVKcXE0124o20xiJQiSMIxg,9259
7
7
  pyserials/validate.py,sha256=Ocs0x0BAV9zwRCgg_J5BGU2kEZV6vVjfDBsR3M9Teus,3564
8
8
  pyserials/write.py,sha256=pN8w78qVsKJjZd_jvPUcZjYp_RJkP7uQzpiXvPOv4lM,1776
9
9
  pyserials/exception/__init__.py,sha256=ZhbggwJUMlTyBhifAivC8ZQxP1Na6lJAwzZs7_YjOSU,151
10
- pyserials/exception/_base.py,sha256=JFg1XFOlTfn3HX_AjaM7lCat1M49rnow8pT8WZwH1gk,1956
11
- pyserials/exception/read.py,sha256=Hvz-Hp2I83WyM8m7JAEa2iVn8o3fOcMhSzG0FvyX1GU,8841
12
- pyserials/exception/update.py,sha256=LZ8_fX9UvqwHIZdANsp9NUaxOEoMHTuOQ6LPff79k3I,4885
13
- pyserials/exception/validate.py,sha256=j-WT2F67NEFitv7mQJhvqzj4duvcgSCbzqioE6iJTDo,9683
14
- PySerials-0.0.0.dev5.dist-info/METADATA,sha256=nmJja_tGErd652uk1PlynTmQ7U-P60uhZyy3vKCCSBY,361
15
- PySerials-0.0.0.dev5.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
16
- PySerials-0.0.0.dev5.dist-info/top_level.txt,sha256=SAks7WjSjdkv3i9Hvt4gY_P7VQbhhYJN5mf5dqx1aao,10
17
- PySerials-0.0.0.dev5.dist-info/RECORD,,
10
+ pyserials/exception/_base.py,sha256=Juns3cyAEV9bZ4_nYuefQnPOrXvpQTY_7peXnoCjlgI,765
11
+ pyserials/exception/read.py,sha256=4_OQ6SUvxR61TN0_1v5SK5MgBeJlgYz8j9vJb_6abNY,9275
12
+ pyserials/exception/update.py,sha256=P0js2-iY2fgO_KLdqedgWE3TTS5Xz15cusZY_wuKIW4,4222
13
+ pyserials/exception/validate.py,sha256=gsvbq1bIHHKlIf-KJc7-PSzDZmYDxUFWiYEApJgcm74,7465
14
+ PySerials-0.0.0.dev6.dist-info/METADATA,sha256=-o3uQuTjzfx7Q7zLSCi9CRHLgDIKtYKAtnLK6HVqzxc,357
15
+ PySerials-0.0.0.dev6.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
16
+ PySerials-0.0.0.dev6.dist-info/top_level.txt,sha256=SAks7WjSjdkv3i9Hvt4gY_P7VQbhhYJN5mf5dqx1aao,10
17
+ PySerials-0.0.0.dev6.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (73.0.1)
2
+ Generator: setuptools (74.1.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,58 +1,26 @@
1
1
  """PySerials base Exception class."""
2
2
 
3
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
4
 
5
+ from typing import TYPE_CHECKING as _TYPE_CHECKING
6
+
7
+ from exceptionman import ReporterException as _ReporterException
9
8
 
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
- }
9
+ if _TYPE_CHECKING:
10
+ from mdit import Document
18
11
 
19
12
 
20
13
  class PySerialsException(_ReporterException):
21
14
  """Base class for all exceptions raised by PySerials."""
22
15
 
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
- ):
16
+ def __init__(self, report: Document):
31
17
  super().__init__(
32
- message=message,
33
- description=description,
34
- message_html=message_html,
35
- description_html=description_html,
36
- report_heading=report_heading,
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
+ }
37
25
  )
38
26
  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,19 +1,19 @@
1
1
  """Exceptions raised by `pyserials.read` module."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Literal as _Literal
4
+ from typing import Literal as _Literal, TYPE_CHECKING as _TYPE_CHECKING
5
5
  from pathlib import Path as _Path
6
6
 
7
7
  import ruamel.yaml as _yaml
8
8
  import json as _json
9
+ import mdit as _mdit
9
10
 
10
- from markitup import html as _html, md as _md
11
11
  from tomlkit.exceptions import TOMLKitError as _TOMLKitError
12
12
 
13
- from pyserials.exception._base import PySerialsException as _PySerialsException
13
+ from pyserials.exception import _base
14
14
 
15
15
 
16
- class PySerialsReadException(_PySerialsException):
16
+ class PySerialsReadException(_base.PySerialsException):
17
17
  """Base class for all exceptions raised by `pyserials.read` module.
18
18
 
19
19
  Attributes
@@ -29,25 +29,28 @@ class PySerialsReadException(_PySerialsException):
29
29
  def __init__(
30
30
  self,
31
31
  source_type: _Literal["file", "string"],
32
- description: str,
33
- description_html: str | _html.Element | None = None,
32
+ problem,
33
+ section: dict | None = None,
34
34
  data_type: _Literal["json", "yaml", "toml"] | None = None,
35
35
  filepath: _Path | None = None,
36
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",
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
+ ),
50
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)
51
54
  self.source_type: _Literal["file", "string"] = source_type
52
55
  self.data_type: _Literal["json", "yaml", "toml"] | None = data_type
53
56
  self.filepath: _Path | None = filepath
@@ -58,8 +61,8 @@ class PySerialsEmptyStringError(PySerialsReadException):
58
61
  """Exception raised when a string to be read is empty."""
59
62
 
60
63
  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)
64
+ problem = f"The string is empty."
65
+ super().__init__(problem=problem, source_type="string", data_type=data_type)
63
66
  return
64
67
 
65
68
 
@@ -67,12 +70,20 @@ class PySerialsInvalidFileExtensionError(PySerialsReadException):
67
70
  """Exception raised when a file to be read has an unrecognized extension."""
68
71
 
69
72
  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."
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."
74
85
  )
75
- super().__init__(description=description, source_type="file", filepath=filepath)
86
+ super().__init__(problem=problem, source_type="file", filepath=filepath)
76
87
  return
77
88
 
78
89
 
@@ -80,8 +91,8 @@ class PySerialsMissingFileError(PySerialsReadException):
80
91
  """Exception raised when a file to be read does not exist."""
81
92
 
82
93
  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)
94
+ problem = f"The file does not exist."
95
+ super().__init__(problem=problem, source_type="file", data_type=data_type, filepath=filepath)
85
96
  return
86
97
 
87
98
 
@@ -89,8 +100,8 @@ class PySerialsEmptyFileError(PySerialsReadException):
89
100
  """Exception raised when a file to be read is empty."""
90
101
 
91
102
  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)
103
+ problem = f"The file is empty."
104
+ super().__init__(problem=problem, source_type="file", data_type=data_type, filepath=filepath)
94
105
  return
95
106
 
96
107
 
@@ -121,17 +132,18 @@ class PySerialsInvalidDataError(PySerialsReadException):
121
132
  self.context_line: int | None = None
122
133
  self.context_column: int | None = None
123
134
  self.context_data_type: str | None = None
135
+ self.data_type = data_type
124
136
 
125
137
  if isinstance(cause, _yaml.YAMLError):
126
138
  self.problem_line = cause.problem_mark.line + 1
127
139
  self.problem_column = cause.problem_mark.column + 1
128
- self.problem_data_type = cause.problem_mark.name
140
+ self.problem_data_type = cause.problem_mark.name.removeprefix("<").removesuffix(">")
129
141
  self.problem = cause.problem.strip()
130
142
  if cause.context:
131
143
  self.context = cause.context.strip()
132
144
  self.context_line = cause.context_mark.line + 1
133
145
  self.context_column = cause.context_mark.column + 1
134
- self.context_data_type = cause.context_mark.name
146
+ self.context_data_type = cause.context_mark.name.removeprefix("<").removesuffix(">")
135
147
  elif isinstance(cause, _json.JSONDecodeError):
136
148
  self.problem = cause.msg
137
149
  self.problem_line = cause.lineno
@@ -141,71 +153,88 @@ class PySerialsInvalidDataError(PySerialsReadException):
141
153
  self.problem_column = cause.col
142
154
  self.problem = cause.args[0].removesuffix(f" at line {self.problem_line} col {self.problem_column}")
143
155
  self.problem = self.problem.strip().capitalize().removesuffix(".")
144
- description = "The data is not valid"
156
+ description = ["The data is not valid"]
145
157
  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)
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
+ )
149
174
  return
150
175
 
151
- def _report_content(self, mode: _Literal["full", "short"], md: bool) -> _html.elem.Ul:
176
+ def _report_content(self) -> dict:
152
177
 
153
178
  def make_tabel(problem, line, column, data_type):
154
- rows = [
155
- [title, value] for title, value in [
179
+ items = [
180
+ _mdit.element.field_list_item(title=title, description=value) for title, value in [
156
181
  ["Description", problem],
157
182
  ["Line Number", line],
158
183
  ["Column Number", column],
159
184
  ["Data Type", data_type],
160
185
  ] if value is not None
161
186
  ]
162
- return _html.elem.table_from_rows(rows_body=rows)
187
+ return _mdit.element.field_list(items)
163
188
 
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
- ]
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
+ }
172
196
  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
- ),
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,
180
219
  )
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
220
  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])
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
- 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
@@ -1,11 +1,12 @@
1
1
  """Exceptions raised by `pyserials.validate` module."""
2
2
 
3
3
  from __future__ import annotations
4
- from typing import Any as _Any, Literal as _Literal
4
+ from typing import Any as _Any
5
5
 
6
6
  import jsonschema as _jsonschema
7
- from markitup.html import elem as _html
8
- from markitup.md import elem as _md
7
+ import mdit as _mdit
8
+ from pyprotocol import Stringable
9
+
9
10
  from pyserials import write as _write
10
11
  from pyserials.exception import _base
11
12
 
@@ -25,23 +26,27 @@ class PySerialsValidateException(_base.PySerialsException):
25
26
 
26
27
  def __init__(
27
28
  self,
28
- description: str,
29
+ problem: str,
29
30
  data: dict | list | str | int | float | bool,
30
31
  schema: dict,
31
32
  validator: _Any,
32
33
  registry: _Any = None,
33
- description_html: str | _html.Element | None = None,
34
+ section: dict | None = None,
34
35
  ):
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",
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,
44
48
  )
49
+ super().__init__(report)
45
50
  self.data = data
46
51
  self.schema = schema
47
52
  self.validator = validator
@@ -60,7 +65,7 @@ class PySerialsInvalidJsonSchemaError(PySerialsValidateException):
60
65
  registry: _Any = None,
61
66
  ):
62
67
  super().__init__(
63
- description="The schema is invalid.",
68
+ problem="The schema is invalid.",
64
69
  data=data,
65
70
  schema=schema,
66
71
  validator=validator,
@@ -81,10 +86,9 @@ class PySerialsJsonSchemaValidationError(PySerialsValidateException):
81
86
  registry: _Any = None,
82
87
  ):
83
88
  self.causes = causes
84
- description, description_html = self._parse_errors()
85
89
  super().__init__(
86
- description=description,
87
- description_html=description_html,
90
+ problem=self._generate_problem_statement(),
91
+ section={idx: self._generate_error_report(error) for idx, error in enumerate(self.causes)},
88
92
  data=data,
89
93
  schema=schema,
90
94
  validator=validator,
@@ -92,133 +96,92 @@ class PySerialsJsonSchemaValidationError(PySerialsValidateException):
92
96
  )
93
97
  return
94
98
 
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
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
130
111
 
131
- def _parse_error(
112
+ def _generate_error_report(
132
113
  self,
133
114
  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}",
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
+ ),
142
122
  ]
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),
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
+ ),
165
143
  ]
144
+ section = {}
166
145
  if error.context:
167
- contexts = []
146
+ context_paths = []
168
147
  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,
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
174
156
  )
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,
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,
190
169
  )
170
+ return doc
191
171
 
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,
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",
198
177
  )
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,
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,
206
183
  )
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
184
+ return admo
222
185
 
223
186
  @staticmethod
224
187
  def _parse_error_message(error: _jsonschema.exceptions.ValidationError) -> str:
@@ -228,16 +191,26 @@ class PySerialsJsonSchemaValidationError(PySerialsValidateException):
228
191
  problem = f"Data {msg}"
229
192
  else:
230
193
  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]
194
+ return f"{problem.removeprefix(".")}."
236
195
 
237
196
  @staticmethod
238
197
  def _create_path(path):
239
198
  return "$." + ".".join(str(path_component) for path_component in path)
240
199
 
241
200
  @staticmethod
242
- def _create_details_summary(title: str, code: str) -> str:
243
- return f"{_html.b(title)}: {_html.code(code)}"
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)