PySerials 0.0.0.dev2__py3-none-any.whl → 0.0.0.dev4__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.
- {PySerials-0.0.0.dev2.dist-info → PySerials-0.0.0.dev4.dist-info}/METADATA +5 -2
- PySerials-0.0.0.dev4.dist-info/RECORD +17 -0
- {PySerials-0.0.0.dev2.dist-info → PySerials-0.0.0.dev4.dist-info}/WHEEL +1 -1
- pyserials/__init__.py +2 -1
- pyserials/compare.py +29 -0
- pyserials/exception/_base.py +52 -4
- pyserials/exception/read.py +163 -71
- pyserials/exception/update.py +97 -89
- pyserials/exception/validate.py +185 -15
- pyserials/nested_dict.py +124 -0
- pyserials/read.py +53 -16
- pyserials/update.py +179 -93
- pyserials/validate.py +23 -12
- pyserials/write.py +27 -11
- PySerials-0.0.0.dev2.dist-info/RECORD +0 -15
- {PySerials-0.0.0.dev2.dist-info → PySerials-0.0.0.dev4.dist-info}/top_level.txt +0 -0
pyserials/exception/validate.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
"""Exceptions raised by `pyserials.validate` module."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Any as _Any, Literal as _Literal
|
|
3
5
|
|
|
4
|
-
|
|
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
|
|
5
11
|
|
|
6
|
-
from pyserials.exception._base import PySerialsException as _PySerialsException
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
class PySerialsValidateException(_PySerialsException):
|
|
13
|
+
class PySerialsValidateException(_base.PySerialsException):
|
|
10
14
|
"""Base class for all exceptions raised by `pyserials.validate` module.
|
|
11
15
|
|
|
12
16
|
Attributes
|
|
@@ -21,53 +25,219 @@ class PySerialsValidateException(_PySerialsException):
|
|
|
21
25
|
|
|
22
26
|
def __init__(
|
|
23
27
|
self,
|
|
24
|
-
|
|
28
|
+
description: str,
|
|
25
29
|
data: dict | list | str | int | float | bool,
|
|
26
30
|
schema: dict,
|
|
27
31
|
validator: _Any,
|
|
32
|
+
registry: _Any = None,
|
|
33
|
+
description_html: str | _html.Element | None = None,
|
|
28
34
|
):
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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",
|
|
32
44
|
)
|
|
33
|
-
super().__init__(message=message)
|
|
34
45
|
self.data = data
|
|
35
46
|
self.schema = schema
|
|
36
47
|
self.validator = validator
|
|
48
|
+
self.registry = registry
|
|
37
49
|
return
|
|
38
50
|
|
|
39
51
|
|
|
40
|
-
class
|
|
41
|
-
"""Exception raised when data validation fails due to the
|
|
52
|
+
class PySerialsInvalidJsonSchemaError(PySerialsValidateException):
|
|
53
|
+
"""Exception raised when data validation fails due to the schema being invalid."""
|
|
42
54
|
|
|
43
55
|
def __init__(
|
|
44
56
|
self,
|
|
45
57
|
data: dict | list | str | int | float | bool,
|
|
46
58
|
schema: dict,
|
|
47
59
|
validator: _Any,
|
|
60
|
+
registry: _Any = None,
|
|
48
61
|
):
|
|
49
62
|
super().__init__(
|
|
50
|
-
|
|
63
|
+
description="The schema is invalid.",
|
|
51
64
|
data=data,
|
|
52
65
|
schema=schema,
|
|
53
66
|
validator=validator,
|
|
67
|
+
registry=registry,
|
|
54
68
|
)
|
|
55
69
|
return
|
|
56
70
|
|
|
57
71
|
|
|
58
|
-
class
|
|
59
|
-
"""Exception raised when data validation fails due to the
|
|
72
|
+
class PySerialsJsonSchemaValidationError(PySerialsValidateException):
|
|
73
|
+
"""Exception raised when data validation fails due to the data being invalid against the schema."""
|
|
60
74
|
|
|
61
75
|
def __init__(
|
|
62
76
|
self,
|
|
77
|
+
causes: list[_jsonschema.exceptions.ValidationError],
|
|
63
78
|
data: dict | list | str | int | float | bool,
|
|
64
79
|
schema: dict,
|
|
65
80
|
validator: _Any,
|
|
81
|
+
registry: _Any = None,
|
|
66
82
|
):
|
|
83
|
+
self.causes = causes
|
|
84
|
+
description, description_html = self._parse_errors()
|
|
67
85
|
super().__init__(
|
|
68
|
-
|
|
86
|
+
description=description,
|
|
87
|
+
description_html=description_html,
|
|
69
88
|
data=data,
|
|
70
89
|
schema=schema,
|
|
71
90
|
validator=validator,
|
|
91
|
+
registry=registry,
|
|
72
92
|
)
|
|
73
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)}"
|
pyserials/nested_dict.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import pyserials as _ps
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NestedDict:
|
|
5
|
+
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
data: dict | None = None,
|
|
9
|
+
template_marker_start: str = "${{",
|
|
10
|
+
template_marker_end: str = "}}",
|
|
11
|
+
template_implicit_root: bool = True,
|
|
12
|
+
):
|
|
13
|
+
self._data = data or {}
|
|
14
|
+
self._templater = _ps.update.TemplateFiller(
|
|
15
|
+
marker_start=template_marker_start,
|
|
16
|
+
marker_end=template_marker_end,
|
|
17
|
+
implicit_root=template_implicit_root,
|
|
18
|
+
)
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def fill(
|
|
22
|
+
self,
|
|
23
|
+
path: str = "",
|
|
24
|
+
always_list: bool = False,
|
|
25
|
+
recursive: bool = True,
|
|
26
|
+
):
|
|
27
|
+
if not path:
|
|
28
|
+
value = self._data
|
|
29
|
+
else:
|
|
30
|
+
value = self.__getitem__(path)
|
|
31
|
+
if not value:
|
|
32
|
+
return
|
|
33
|
+
filled_value = self.fill_data(data=value, current_path=path, always_list=always_list, recursive=recursive)
|
|
34
|
+
if not path:
|
|
35
|
+
self._data = filled_value
|
|
36
|
+
else:
|
|
37
|
+
self.__setitem__(path, filled_value)
|
|
38
|
+
return filled_value
|
|
39
|
+
|
|
40
|
+
def fill_data(
|
|
41
|
+
self,
|
|
42
|
+
data,
|
|
43
|
+
current_path: str = "",
|
|
44
|
+
always_list: bool = False,
|
|
45
|
+
recursive: bool = True,
|
|
46
|
+
):
|
|
47
|
+
return self._templater.fill(
|
|
48
|
+
templated_data=data,
|
|
49
|
+
source_data=self._data,
|
|
50
|
+
current_path=current_path,
|
|
51
|
+
always_list=always_list,
|
|
52
|
+
recursive=recursive,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def __call__(self):
|
|
56
|
+
return self._data
|
|
57
|
+
|
|
58
|
+
def __getitem__(self, item: str):
|
|
59
|
+
keys = item.split(".")
|
|
60
|
+
data = self._data
|
|
61
|
+
for key in keys:
|
|
62
|
+
if not isinstance(data, dict):
|
|
63
|
+
raise KeyError(f"Key '{key}' not found in '{data}'.")
|
|
64
|
+
if key not in data:
|
|
65
|
+
return
|
|
66
|
+
data = data[key]
|
|
67
|
+
# if isinstance(data, dict):
|
|
68
|
+
# return NestedDict(data)
|
|
69
|
+
# if isinstance(data, list) and all(isinstance(item, dict) for item in data):
|
|
70
|
+
# return [NestedDict(item) for item in data]
|
|
71
|
+
return data
|
|
72
|
+
|
|
73
|
+
def __setitem__(self, key, value):
|
|
74
|
+
key = key.split(".")
|
|
75
|
+
data = self._data
|
|
76
|
+
for k in key[:-1]:
|
|
77
|
+
if k not in data:
|
|
78
|
+
data[k] = {}
|
|
79
|
+
data = data[k]
|
|
80
|
+
data[key[-1]] = value
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
def __contains__(self, item):
|
|
84
|
+
keys = item.split(".")
|
|
85
|
+
data = self._data
|
|
86
|
+
for key in keys:
|
|
87
|
+
if not isinstance(data, dict) or key not in data:
|
|
88
|
+
return False
|
|
89
|
+
data = data[key]
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
def __bool__(self):
|
|
93
|
+
return bool(self._data)
|
|
94
|
+
|
|
95
|
+
def setdefault(self, key, value):
|
|
96
|
+
key = key.split(".")
|
|
97
|
+
data = self._data
|
|
98
|
+
for k in key[:-1]:
|
|
99
|
+
if k not in data:
|
|
100
|
+
data[k] = {}
|
|
101
|
+
data = data[k]
|
|
102
|
+
return data.setdefault(key[-1], value)
|
|
103
|
+
|
|
104
|
+
def get(self, key, default=None):
|
|
105
|
+
keys = key.split(".")
|
|
106
|
+
data = self._data
|
|
107
|
+
for key in keys:
|
|
108
|
+
if not isinstance(data, dict) or key not in data:
|
|
109
|
+
return default
|
|
110
|
+
data = data[key]
|
|
111
|
+
return data
|
|
112
|
+
|
|
113
|
+
def items(self):
|
|
114
|
+
return self._data.items()
|
|
115
|
+
|
|
116
|
+
def keys(self):
|
|
117
|
+
return self._data.keys()
|
|
118
|
+
|
|
119
|
+
def values(self):
|
|
120
|
+
return self._data.values()
|
|
121
|
+
|
|
122
|
+
def update(self, data: dict):
|
|
123
|
+
self._data.update(data)
|
|
124
|
+
return
|
pyserials/read.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Literal as _Literal
|
|
1
|
+
from typing import Literal as _Literal, Callable as _Callable, Any as _Any
|
|
2
2
|
import json as _json
|
|
3
3
|
from pathlib import Path as _Path
|
|
4
4
|
from functools import partial as _partial
|
|
@@ -17,11 +17,11 @@ def from_file(
|
|
|
17
17
|
toml_as_dict: bool = False,
|
|
18
18
|
):
|
|
19
19
|
if data_type is None:
|
|
20
|
-
data_type = _Path(path).suffix.removeprefix(".")
|
|
20
|
+
data_type = _Path(path).suffix.removeprefix(".").lower()
|
|
21
21
|
if data_type == "yml":
|
|
22
22
|
data_type = "yaml"
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
if data_type not in ("json", "yaml", "toml"):
|
|
24
|
+
raise _exception.read.PySerialsInvalidFileExtensionError(filepath=path)
|
|
25
25
|
if data_type == "json":
|
|
26
26
|
return json_from_file(path=path, strict=json_strict)
|
|
27
27
|
if data_type == "yaml":
|
|
@@ -97,32 +97,56 @@ def json_from_file(path: str | _Path, strict: bool = True) -> dict | list | str
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def yaml(
|
|
100
|
-
source: str | _Path,
|
|
100
|
+
source: str | _Path,
|
|
101
|
+
safe: bool = True,
|
|
102
|
+
constructors: dict[str, _Callable[[_yaml.Constructor, _yaml.ScalarNode], _Any]] | None = None,
|
|
101
103
|
) -> dict | list | str | int | float | bool | _yaml.CommentedMap | _yaml.CommentedSeq:
|
|
104
|
+
"""Load YAML data from a file or string.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
source : str | _Path
|
|
109
|
+
Path to the file or YAML data string.
|
|
110
|
+
safe : bool, default: True
|
|
111
|
+
Use safe YAML loader.
|
|
112
|
+
constructors : dict[str, Callable], default: None
|
|
113
|
+
Custom YAML constructors.
|
|
114
|
+
|
|
115
|
+
References
|
|
116
|
+
----------
|
|
117
|
+
- See `add_constructor` method in
|
|
118
|
+
`ruamel.yaml.constructor.SafeConstructor` and `ruamel.yaml.constructor.RoundTripConstructor`.
|
|
119
|
+
- See `add_constructor` in [PyYAML documentation](https://pyyaml.org/wiki/PyYAMLDocumentation).
|
|
120
|
+
- See Tags in [YAML documentation](https://yaml.org/spec/1.2.0/#id2560445)
|
|
121
|
+
"""
|
|
102
122
|
if isinstance(source, str):
|
|
103
|
-
return yaml_from_string(data=source, safe=safe)
|
|
123
|
+
return yaml_from_string(data=source, safe=safe, constructors=constructors)
|
|
104
124
|
return yaml_from_file(path=source, safe=safe)
|
|
105
125
|
|
|
106
126
|
|
|
107
127
|
def yaml_from_string(
|
|
108
|
-
data: str,
|
|
128
|
+
data: str,
|
|
129
|
+
safe: bool = True,
|
|
130
|
+
constructors: dict[str, _Callable[[_yaml.Constructor, _yaml.ScalarNode], _Any]] | None = None,
|
|
109
131
|
) -> dict | list | str | int | float | bool | _yaml.CommentedMap | _yaml.CommentedSeq:
|
|
110
132
|
content = _read_from_string(
|
|
111
133
|
data=data,
|
|
112
134
|
data_type="yaml",
|
|
113
|
-
loader=
|
|
135
|
+
loader=_make_yaml_loader(safe=safe, constructors=constructors).load,
|
|
114
136
|
exception=_yaml.YAMLError
|
|
115
137
|
)
|
|
116
138
|
return content
|
|
117
139
|
|
|
118
140
|
|
|
119
141
|
def yaml_from_file(
|
|
120
|
-
path: str | _Path,
|
|
142
|
+
path: str | _Path,
|
|
143
|
+
safe: bool = True,
|
|
144
|
+
constructors: dict[str, _Callable[[_yaml.Constructor, _yaml.ScalarNode], _Any]] | None = None,
|
|
121
145
|
) -> dict | list | str | int | float | bool | _yaml.CommentedMap | _yaml.CommentedSeq:
|
|
122
146
|
content = _read_from_file(
|
|
123
147
|
path=path,
|
|
124
148
|
data_type="yaml",
|
|
125
|
-
loader=
|
|
149
|
+
loader=_make_yaml_loader(safe=safe, constructors=constructors).load,
|
|
126
150
|
exception=_yaml.YAMLError
|
|
127
151
|
)
|
|
128
152
|
return content
|
|
@@ -136,16 +160,16 @@ def _read_from_file(
|
|
|
136
160
|
):
|
|
137
161
|
path = _Path(path).resolve()
|
|
138
162
|
if not path.is_file():
|
|
139
|
-
raise _exception.read.PySerialsMissingFileError(filepath=path)
|
|
163
|
+
raise _exception.read.PySerialsMissingFileError(data_type=data_type, filepath=path)
|
|
140
164
|
data = path.read_text()
|
|
141
165
|
if data.strip() == "":
|
|
142
|
-
raise _exception.read.PySerialsEmptyFileError(filepath=path)
|
|
166
|
+
raise _exception.read.PySerialsEmptyFileError(data_type=data_type, filepath=path)
|
|
143
167
|
try:
|
|
144
168
|
content = loader(data)
|
|
145
169
|
except exception as e:
|
|
146
|
-
raise _exception.read.
|
|
147
|
-
filepath=path, data_type=data_type, data=data
|
|
148
|
-
) from
|
|
170
|
+
raise _exception.read.PySerialsInvalidDataError(
|
|
171
|
+
source_type="file", filepath=path, data_type=data_type, data=data, cause=e
|
|
172
|
+
) from None
|
|
149
173
|
return content
|
|
150
174
|
|
|
151
175
|
|
|
@@ -160,5 +184,18 @@ def _read_from_string(
|
|
|
160
184
|
try:
|
|
161
185
|
content = loader(data)
|
|
162
186
|
except exception as e:
|
|
163
|
-
raise _exception.read.
|
|
187
|
+
raise _exception.read.PySerialsInvalidDataError(
|
|
188
|
+
source_type="string", data_type=data_type, data=data, cause=e
|
|
189
|
+
) from None
|
|
164
190
|
return content
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _make_yaml_loader(
|
|
194
|
+
safe: bool,
|
|
195
|
+
constructors: dict[str, _Callable[[_yaml.Constructor, _yaml.ScalarNode], _Any]] | None = None
|
|
196
|
+
) -> _yaml.YAML:
|
|
197
|
+
loader = _yaml.YAML(typ="safe" if safe else "rt")
|
|
198
|
+
if constructors:
|
|
199
|
+
for key, constructor in constructors.items():
|
|
200
|
+
loader.constructor.add_constructor(key, constructor)
|
|
201
|
+
return loader
|