PySerials 0.0.0.dev3__py3-none-any.whl → 0.0.0.dev5__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,9 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: PySerials
3
- Version: 0.0.0.dev3
3
+ Version: 0.0.0.dev5
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
12
+ Requires-Dist: jsonpath-ng <2,>=1.6.1
9
13
 
@@ -0,0 +1,17 @@
1
+ pyserials/__init__.py,sha256=-ySdqDuoUXdi2Pa8uuFa5m1CTAtbZS3SWc5qzaOdR5o,142
2
+ pyserials/compare.py,sha256=j62A1UIiAm08_xONlbZmU2EcH1GMEpDyEQH66dZ2YMM,1297
3
+ pyserials/format.py,sha256=dTukpab6WHSyVRQ9SteY5fhr3GFjWFboEl-1cw_udVY,1729
4
+ pyserials/nested_dict.py,sha256=8cPs4LykXh4stFpaLINPjrAeSyaOyyTdhg0FZZ2YOYA,3361
5
+ pyserials/read.py,sha256=uucYQH1V4GStwRgRZ2eQIXkH4ukB5qz0EA885grwi68,6592
6
+ pyserials/update.py,sha256=GfrFgc1a2qYqR8zs04cngVKcXE0124o20xiJQiSMIxg,9259
7
+ pyserials/validate.py,sha256=Ocs0x0BAV9zwRCgg_J5BGU2kEZV6vVjfDBsR3M9Teus,3564
8
+ pyserials/write.py,sha256=pN8w78qVsKJjZd_jvPUcZjYp_RJkP7uQzpiXvPOv4lM,1776
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (73.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
pyserials/__init__.py CHANGED
@@ -1,3 +1,4 @@
1
1
  """PySerials"""
2
2
 
3
- from pyserials import exception, update, validate, read, write, format
3
+ from pyserials import exception, update, validate, read, write, format, compare
4
+ from pyserials.nested_dict import NestedDict
pyserials/compare.py ADDED
@@ -0,0 +1,29 @@
1
+ def items(source, target, path: str = "$"):
2
+ def recursive_compare(src, trg, curr_path):
3
+ if type(src) is not type(trg):
4
+ comp["modified"].append(curr_path)
5
+ return
6
+ if isinstance(src, dict):
7
+ for key in src:
8
+ if key not in trg:
9
+ comp["added"].append(f"{curr_path}.{key}")
10
+ continue
11
+ recursive_compare(src[key], trg[key], f"{curr_path}.{key}")
12
+ for key in trg:
13
+ if key not in src:
14
+ comp["removed"].append(f"{curr_path}.{key}")
15
+ return
16
+ if isinstance(src, (list, tuple)):
17
+ len_src = len(src)
18
+ len_trg = len(trg)
19
+ min_len = min(len_src, len_trg)
20
+ for i in range(min_len):
21
+ recursive_compare(src[i], trg[i], f"{curr_path}[{i}]")
22
+ for i in range(min_len, max(len_src, len_trg)):
23
+ comp["added" if len_src > len_trg else "removed"].append(f"{curr_path}[{i}]")
24
+ return
25
+ comp["unchanged" if src == trg else "modified"].append(curr_path)
26
+ return
27
+ comp = {"added": [], "removed": [], "modified": [], "unchanged": []}
28
+ recursive_compare(source, target, path)
29
+ return {key: sorted(comp[key]) for key in comp}
@@ -1,10 +1,58 @@
1
1
  """PySerials base Exception class."""
2
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
3
8
 
4
- class PySerialsException(Exception):
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):
5
21
  """Base class for all exceptions raised by PySerials."""
6
22
 
7
- def __init__(self, message: str):
8
- self.message = message
9
- super().__init__(message)
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
+ )
10
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,119 +1,211 @@
1
1
  """Exceptions raised by `pyserials.read` module."""
2
2
 
3
-
3
+ from __future__ import annotations
4
4
  from typing import Literal as _Literal
5
5
  from pathlib import Path as _Path
6
6
 
7
- from pyserials.exception._base import PySerialsException as _PySerialsException
7
+ import ruamel.yaml as _yaml
8
+ import json as _json
8
9
 
10
+ from markitup import html as _html, md as _md
11
+ from tomlkit.exceptions import TOMLKitError as _TOMLKitError
9
12
 
10
- class PySerialsReadException(_PySerialsException):
11
- """Base class for all exceptions raised by `pyserials.read` module."""
12
-
13
- def __init__(self, message: str, source_type: _Literal["file", "string"]):
14
- super().__init__(message=f"Failed to read data from {source_type}. {message}.")
15
- return
13
+ from pyserials.exception._base import PySerialsException as _PySerialsException
16
14
 
17
15
 
18
- class PySerialsReadFromFileException(PySerialsReadException):
19
- """Base class for all exceptions raised when reading data from a file.
16
+ class PySerialsReadException(_PySerialsException):
17
+ """Base class for all exceptions raised by `pyserials.read` module.
20
18
 
21
19
  Attributes
22
20
  ----------
23
- filepath : pathlib.Path
24
- The path of the input datafile.
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.
25
27
  """
26
- def __init__(self, message: str, filepath: _Path):
27
- super().__init__(message=message, source_type="file")
28
- self.filepath: _Path = filepath
29
- return
30
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
31
55
 
32
- class PySerialsReadFromStringException(PySerialsReadException):
33
- """Base class for all exceptions raised when reading data from a string.
34
56
 
35
- Attributes
36
- ----------
37
- data_type : {"json", "yaml", "toml"}
38
- The type of data.
39
- """
57
+ class PySerialsEmptyStringError(PySerialsReadException):
58
+ """Exception raised when a string to be read is empty."""
40
59
 
41
- def __init__(self, message: str, data_type: _Literal["json", "yaml", "toml"]):
42
- super().__init__(message=message, source_type="string")
43
- self.data_type = data_type
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)
44
63
  return
45
64
 
46
65
 
47
- class PySerialsInvalidFileExtensionError(PySerialsReadFromFileException):
66
+ class PySerialsInvalidFileExtensionError(PySerialsReadException):
48
67
  """Exception raised when a file to be read has an unrecognized extension."""
49
68
 
50
69
  def __init__(self, filepath: _Path):
51
- message = (
52
- f"The extension of the file at '{filepath}' is invalid; "
53
- f"expected one of 'json', 'yaml', or 'toml', but got '{filepath.suffix.removeprefix('.')}'. "
70
+ description = (
71
+ f"The file extension must be one of 'json', 'yaml', 'yml', or '.toml', "
72
+ f"but got '{filepath.suffix.removeprefix('.')}'. "
54
73
  "Please provide the extension explicitly, or rename the file to have a valid extension."
55
74
  )
56
- super().__init__(message=message, filepath=filepath)
75
+ super().__init__(description=description, source_type="file", filepath=filepath)
57
76
  return
58
77
 
59
78
 
60
- class PySerialsMissingFileError(PySerialsReadFromFileException):
79
+ class PySerialsMissingFileError(PySerialsReadException):
61
80
  """Exception raised when a file to be read does not exist."""
62
81
 
63
- def __init__(self, filepath: _Path):
64
- message = f"The file at '{filepath}' does not exist."
65
- super().__init__(message=message, filepath=filepath)
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)
66
85
  return
67
86
 
68
87
 
69
- class PySerialsEmptyFileError(PySerialsReadFromFileException):
88
+ class PySerialsEmptyFileError(PySerialsReadException):
70
89
  """Exception raised when a file to be read is empty."""
71
90
 
72
- def __init__(self, filepath: _Path):
73
- message = f"The file at '{filepath}' is empty."
74
- super().__init__(message=message, filepath=filepath)
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)
75
94
  return
76
95
 
77
96
 
78
- class PySerialsInvalidDataFileError(PySerialsReadFromFileException):
79
- """Exception raised when a file to be read is invalid.
97
+ class PySerialsInvalidDataError(PySerialsReadException):
98
+ """Exception raised when the data is invalid.
80
99
 
81
100
  Attributes
82
101
  ----------
83
102
  data : str
84
103
  The input data that was supposed to be read.
85
- data_type : {"json", "yaml", "toml"}
86
- The type of data.
87
104
  """
88
105
 
89
- def __init__(self, filepath: _Path, data_type: _Literal["json", "yaml", "toml"], data: str):
90
- message = f"The {data_type} file at '{filepath}' is invalid."
91
- super().__init__(message=message, filepath=filepath)
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
+ ):
92
114
  self.data = data
93
- self.data_type = data_type
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)
94
149
  return
95
150
 
96
-
97
- class PySerialsEmptyStringError(PySerialsReadFromStringException):
98
- """Exception raised when a string to be read is empty."""
99
-
100
- def __init__(self, data_type: _Literal["json", "yaml", "toml"]):
101
- message = f"The {data_type} string is empty."
102
- super().__init__(message=message, data_type=data_type)
103
- return
104
-
105
-
106
- class PySerialsInvalidDataStringError(PySerialsReadFromStringException):
107
- """Exception raised when a string to be read is invalid.
108
-
109
- Attributes
110
- ----------
111
- data : str
112
- The input data that was supposed to be read.
113
- """
114
-
115
- def __init__(self, data_type: _Literal["json", "yaml", "toml"], data: str):
116
- message = f"The {data_type} string is invalid."
117
- super().__init__(message=message, data_type=data_type)
118
- self.data = data
119
- return
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,131 +1,139 @@
1
1
  """Exceptions raised by `pyserials.update` module."""
2
2
 
3
- from typing import Any as _Any
3
+ from __future__ import annotations
4
+ from typing import Any as _Any, Literal as _Literal
4
5
 
5
- from pyserials.exception._base import PySerialsException as _PySerialsException
6
+ from markitup.html import elem as _html
6
7
 
8
+ from pyserials.exception import _base
7
9
 
8
- class PySerialsUpdateException(_PySerialsException):
9
- """Base class for all exceptions raised by `pyserials.update` module."""
10
10
 
11
- def __init__(self, message: str):
12
- super().__init__(f"Failed to update data. {message}.")
13
- return
14
-
15
-
16
- class PySerialsUpdateDictFromAddonException(PySerialsUpdateException):
17
- """Base class for all exceptions raised by `pyserials.update.dict_from_addon`.
11
+ class PySerialsUpdateException(_base.PySerialsException):
12
+ """Base class for all exceptions raised by `pyserials.update` module.
18
13
 
19
14
  Attributes
20
15
  ----------
21
- address : str
22
- The address of the data that failed to update.
23
- value_data : Any
24
- The value of the data in the source dictionary.
25
- value_addon : Any
26
- The value of the data in the addon dictionary.
16
+ path : str
17
+ JSONPath to where the update failed.
18
+ data : dict | list | str | int | float | bool
19
+ Data that failed to update.
20
+ data_full : dict | list | str | int | float | bool
21
+ Full data input.
27
22
  """
28
23
 
29
24
  def __init__(
30
25
  self,
31
- message: str,
32
- address: str,
33
- value_data: _Any,
34
- value_addon: _Any,
26
+ path: str,
27
+ data: dict | list | str | int | float | bool,
28
+ data_full: dict | list | str | int | float | bool,
29
+ description: str,
30
+ description_html: str | _html.Element | None = None,
35
31
  ):
36
- super().__init__(message=message)
37
- self.address = address
38
- self.value_data = value_data
39
- self.value_addon = value_addon
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",
40
+ )
41
+ self.path = path
42
+ self.data = data
43
+ self.data_full = data_full
40
44
  return
41
45
 
42
46
 
43
- class PySerialsUpdateTemplatedDataException(PySerialsUpdateException):
44
- """Base class for all exceptions raised by `pyserials.update.templated_data_from_source`.
47
+ class PySerialsUpdateDictFromAddonError(PySerialsUpdateException):
48
+ """Base class for all exceptions raised by `pyserials.update.dict_from_addon`.
45
49
 
46
50
  Attributes
47
51
  ----------
48
- templated_data : str
49
- The templated data that failed to update.
50
- source_data : dict
51
- The data that was used to update the template.
52
- template_start : str
53
- The start marker of the template.
54
- template_end : str
55
- The end marker of the template.
52
+ data_addon : Any
53
+ Value of the failed data in the addon dictionary.
54
+ data_addon_full : dictionary
55
+ Full addon input.
56
56
  """
57
57
 
58
58
  def __init__(
59
59
  self,
60
- message: str,
61
- templated_data: str,
62
- source_data: dict,
63
- template_start: str,
64
- template_end: str
60
+ problem_type: _Literal["duplicate", "type_mismatch"],
61
+ path: str,
62
+ data: _Any,
63
+ data_full: dict,
64
+ data_addon: _Any,
65
+ data_addon_full: dict,
65
66
  ):
66
- super().__init__(message=message)
67
- self.templated_data = templated_data
68
- self.source_data = source_data
69
- self.template_start = template_start
70
- self.template_end = template_end
71
- return
72
-
73
-
74
- class PySerialsDictUpdateTypeMismatchError(PySerialsUpdateDictFromAddonException):
75
- """Exception raised when a dict update fails due to a type mismatch."""
76
-
77
- def __init__(self, address: str, value_data: _Any, value_addon: _Any):
78
- message = (
79
- f"There was a type mismatch between the source and addon dictionary values at '{address}'; "
80
- f"the value is of type '{type(value_data).__name__}' in the source data, "
81
- f"but of type '{type(value_addon).__name__}' in the addon data"
67
+ self.type_data = type(data)
68
+ 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
+ )
82
79
  )
83
- super().__init__(message=message, address=address, value_data=value_data, value_addon=value_addon)
84
- return
85
-
86
-
87
- class PySerialsDictUpdateDuplicationError(PySerialsUpdateDictFromAddonException):
88
- """Exception raised when a dict update fails due to a duplication."""
89
-
90
- def __init__(self, address: str, value_data: _Any, value_addon: _Any):
91
- message = (
92
- f"There was a duplication in the addon dictionary at '{address}'; "
93
- f"the value of type '{type(value_addon).__name__}' already exists in the source data"
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."
87
+ )
88
+ super().__init__(
89
+ description=description_template.format(**kwargs_console),
90
+ description_html=description_template.format(**kwargs_html),
91
+ path=path,
92
+ data=data,
93
+ data_full=data_full,
94
94
  )
95
- super().__init__(message=message, address=address, value_data=value_data, value_addon=value_addon)
95
+ self.problem_type: _Literal["duplicate", "type_mismatch"] = problem_type
96
+ self.data_addon = data_addon
97
+ self.data_addon_full = data_addon_full
96
98
  return
97
99
 
98
100
 
99
- class PySerialsTemplateUpdateMissingSourceError(PySerialsUpdateTemplatedDataException):
100
- """Exception raised when a templated data update fails due to a missing key in source data.
101
+ class PySerialsUpdateTemplatedDataError(PySerialsUpdateException):
102
+ """Exception raised when updating templated data fails.
101
103
 
102
104
  Attributes
103
105
  ----------
104
- address_full : str
105
- The full address in the source data where the key/index is missing.
106
- address_missing : str
107
- The key/index that is missing in the source data at `address_full`.
106
+ path_invalid : str
107
+ JSONPath that caused the update to fail.
108
+ data_source : dict
109
+ Source data that was used to update the template.
110
+ template_start : str
111
+ The start marker of the template.
112
+ template_end : str
113
+ The end marker of the template.
108
114
  """
109
115
 
110
116
  def __init__(
111
117
  self,
112
- address_full: str,
113
- address_missing: str,
114
- templated_data: str,
115
- source_data: dict,
118
+ description_template: str,
119
+ path_invalid: str,
120
+ path: str,
121
+ data: str,
122
+ data_full: dict | list | str | int | float | bool,
123
+ data_source: dict,
116
124
  template_start: str,
117
- template_end: str
125
+ template_end: str,
118
126
  ):
119
- message = (
120
- f"The key/index '{address_missing}' is missing in the source data at '{address_full}'"
121
- )
127
+ path_invalid_console, path_invalid_html = _base.format_code(path_invalid.replace("'", ""))
122
128
  super().__init__(
123
- message=message,
124
- templated_data=templated_data,
125
- source_data=source_data,
126
- template_start=template_start,
127
- template_end=template_end
129
+ description=description_template.format(path_invalid=path_invalid_console),
130
+ description_html=description_template.format(path_invalid=path_invalid_html),
131
+ path=path.replace("'", ""),
132
+ data=data,
133
+ data_full=data_full,
128
134
  )
129
- self.address_full = address_full
130
- self.address_missing = address_missing
135
+ self.path_invalid = path_invalid
136
+ self.data_source = data_source
137
+ self.template_start = template_start
138
+ self.template_end = template_end
131
139
  return