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