PySerials 0.1.2__tar.gz → 0.1.3__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 (36) hide show
  1. {pyserials-0.1.2 → pyserials-0.1.3}/PKG-INFO +3 -3
  2. {pyserials-0.1.2 → pyserials-0.1.3}/pyproject.toml +4 -4
  3. pyserials-0.1.3/setup.py +141 -0
  4. {pyserials-0.1.2 → pyserials-0.1.3}/src/PySerials.egg-info/PKG-INFO +3 -3
  5. {pyserials-0.1.2 → pyserials-0.1.3}/src/PySerials.egg-info/SOURCES.txt +1 -0
  6. {pyserials-0.1.2 → pyserials-0.1.3}/src/PySerials.egg-info/requires.txt +1 -1
  7. pyserials-0.1.3/src/pyserials/__init__.py +38 -0
  8. pyserials-0.1.3/src/pyserials/compare.py +88 -0
  9. pyserials-0.1.3/src/pyserials/exception/__init__.py +8 -0
  10. {pyserials-0.1.2 → pyserials-0.1.3}/src/pyserials/exception/_base.py +17 -7
  11. {pyserials-0.1.2 → pyserials-0.1.3}/src/pyserials/exception/read.py +108 -43
  12. {pyserials-0.1.2 → pyserials-0.1.3}/src/pyserials/exception/update.py +46 -32
  13. {pyserials-0.1.2 → pyserials-0.1.3}/src/pyserials/exception/validate.py +57 -32
  14. pyserials-0.1.3/src/pyserials/flatten.py +98 -0
  15. pyserials-0.1.3/src/pyserials/format.py +167 -0
  16. pyserials-0.1.3/src/pyserials/nested_dict.py +390 -0
  17. pyserials-0.1.3/src/pyserials/property_dict.py +154 -0
  18. pyserials-0.1.3/src/pyserials/read.py +511 -0
  19. pyserials-0.1.3/src/pyserials/update.py +1909 -0
  20. pyserials-0.1.3/src/pyserials/validate.py +135 -0
  21. pyserials-0.1.3/src/pyserials/write.py +295 -0
  22. pyserials-0.1.2/src/pyserials/__init__.py +0 -6
  23. pyserials-0.1.2/src/pyserials/compare.py +0 -29
  24. pyserials-0.1.2/src/pyserials/exception/__init__.py +0 -4
  25. pyserials-0.1.2/src/pyserials/flatten.py +0 -51
  26. pyserials-0.1.2/src/pyserials/format.py +0 -57
  27. pyserials-0.1.2/src/pyserials/nested_dict.py +0 -174
  28. pyserials-0.1.2/src/pyserials/property_dict.py +0 -85
  29. pyserials-0.1.2/src/pyserials/read.py +0 -201
  30. pyserials-0.1.2/src/pyserials/update.py +0 -763
  31. pyserials-0.1.2/src/pyserials/validate.py +0 -89
  32. pyserials-0.1.2/src/pyserials/write.py +0 -123
  33. {pyserials-0.1.2 → pyserials-0.1.3}/setup.cfg +0 -0
  34. {pyserials-0.1.2 → pyserials-0.1.3}/src/PySerials.egg-info/dependency_links.txt +0 -0
  35. {pyserials-0.1.2 → pyserials-0.1.3}/src/PySerials.egg-info/not-zip-safe +0 -0
  36. {pyserials-0.1.2 → pyserials-0.1.3}/src/PySerials.egg-info/top_level.txt +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySerials
3
- Version: 0.1.2
4
- Requires-Python: >=3.10
3
+ Version: 0.1.3
4
+ Requires-Python: >=3.12
5
5
  Requires-Dist: jsonschema<5,>=4.21.0
6
6
  Requires-Dist: referencing>=0.35.1
7
- Requires-Dist: jsonpath-ng<2,>=1.6.1
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
10
  Requires-Dist: tomlkit<0.14,>=0.11.8
@@ -1,6 +1,6 @@
1
1
 
2
2
  [build-system]
3
- requires = ["setuptools>=61.0", "versioningit"]
3
+ requires = ["setuptools>=61.0", "versioningit", "mypy>=1.0"]
4
4
  build-backend = "setuptools.build_meta"
5
5
 
6
6
 
@@ -17,12 +17,12 @@ namespaces = true
17
17
  # ----------------------------------------- Project Metadata -------------------------------------
18
18
  #
19
19
  [project]
20
- version = "0.1.2"
20
+ version = "0.1.3"
21
21
  name = "PySerials"
22
22
  dependencies = [
23
23
  "jsonschema >= 4.21.0, < 5",
24
24
  "referencing >= 0.35.1",
25
- "jsonpath-ng >= 1.6.1, < 2",
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
28
  "tomlkit >= 0.11.8, < 0.14", # https://tomlkit.readthedocs.io/en/stable/,
@@ -30,4 +30,4 @@ dependencies = [
30
30
  "ExceptionMan >=0.1,<0.2",
31
31
  "ProtocolMan >=0.1,<0.2",
32
32
  ]
33
- requires-python = ">=3.10"
33
+ requires-python = ">=3.12"
@@ -0,0 +1,141 @@
1
+ """PySerials Setup Script
2
+
3
+ Usage
4
+ -----
5
+ - Build with mypyc (default when available):
6
+ ```
7
+ python -m build
8
+ ```
9
+
10
+ - Development install with compiled extensions:
11
+ ```
12
+ pip install -e .
13
+ ```
14
+
15
+ - Force pure-Python build even when compilers are available:
16
+ ```
17
+ DISABLE_MYPYC=1 pip install -e .
18
+ ```
19
+
20
+ Notes
21
+ -----
22
+ Compiled extensions are built when **both** conditions are met:
23
+
24
+ 1. **DISABLE_MYPYC** — The environment variable is not set to a truthy
25
+ value (``1``, ``true``, or ``yes``, case-insensitive).
26
+ Defaults to unset (enabled).
27
+
28
+ 2. **SHOULD_COMPILE** — The setup command includes a compilation-triggering
29
+ argument: ``bdist_wheel``, ``editable_wheel``, ``build_ext``, ``build``,
30
+ ``install``, or ``develop``.
31
+ Commands like ``egg_info`` or ``sdist`` skip compilation entirely.
32
+
33
+ If build dependencies are missing or compilation fails,
34
+ the build raises an error and stops.
35
+
36
+ mypyc modules
37
+ ~~~~~~~~~~~~~
38
+ Only modules with measured speedup and no subclassing risk are compiled:
39
+
40
+ - ``pyserials.compare`` — Recursive dict/list comparison (2.2x speedup).
41
+ - ``pyserials.flatten`` — Nested-to-flat dict conversion (2.4x speedup).
42
+ - ``pyserials.format`` — TOML/YAML object conversion (1.3–2.6x speedup).
43
+ - ``pyserials.update`` — Template filler & recursive update (1.4–2.5x speedup).
44
+
45
+ Modules intentionally kept as pure Python:
46
+
47
+ - ``pyserials.read`` / ``pyserials.write`` — I/O-bound; ~98% time in
48
+ ruamel.yaml / tomlkit / json. Compilation yields only 1.1x.
49
+ - ``pyserials.validate`` — Actually *slower* compiled (0.5–0.9x) due to
50
+ mypyc boxing overhead at the untyped jsonschema boundary.
51
+ - ``pyserials.property_dict`` / ``pyserials.nested_dict`` — Compiled classes
52
+ cannot be subclassed from interpreted Python, which would be a breaking
53
+ change for downstream users.
54
+ """
55
+
56
+ import logging
57
+ import os
58
+ import sys
59
+
60
+ from setuptools import setup, Extension
61
+
62
+
63
+ SRC_DIR: str = "src/pyserials"
64
+
65
+ log = logging.getLogger("pyserials.setup")
66
+ logging.basicConfig(level=logging.INFO, format="%(name)s: %(message)s")
67
+
68
+ COMPILE_COMMANDS = {
69
+ "bdist_wheel",
70
+ "editable_wheel",
71
+ "build_ext",
72
+ "build",
73
+ "install",
74
+ "develop",
75
+ }
76
+ IS_COMPILE_COMMAND = bool(set(sys.argv) & COMPILE_COMMANDS)
77
+ DISABLE_MYPYC = os.environ.get("DISABLE_MYPYC", "").lower() in ("1", "true", "yes")
78
+
79
+ # Number of parallel compilation jobs (C compiler invocations).
80
+ # Defaults to os.cpu_count(); override with BUILD_PARALLEL=N.
81
+ PARALLEL = int(os.environ.get("BUILD_PARALLEL", os.cpu_count() or 1))
82
+
83
+ # mypyc modules to compile (hot-path only; keep error handling in pure Python).
84
+ MYPYC_MODULES = [
85
+ f"{SRC_DIR}/compare.py",
86
+ f"{SRC_DIR}/flatten.py",
87
+ f"{SRC_DIR}/format.py",
88
+ f"{SRC_DIR}/update.py",
89
+ ]
90
+
91
+
92
+ def create_mypyc_extensions() -> list[Extension]:
93
+ """Create mypyc-compiled extensions for performance-critical modules."""
94
+ try:
95
+ from mypyc.build import mypycify
96
+ except ImportError as exc:
97
+ log.critical("mypyc dependency not found: %s", exc)
98
+ raise RuntimeError("mypyc dependency not found") from exc
99
+
100
+ try:
101
+ log.info("mypyc modules: %s", MYPYC_MODULES)
102
+ ext_modules = mypycify(
103
+ ["--ignore-missing-imports", *MYPYC_MODULES],
104
+ opt_level="3",
105
+ debug_level="0",
106
+ strip_asserts=True,
107
+ multi_file=False,
108
+ verbose=True,
109
+ )
110
+ log.info("Compiling %d mypyc extensions", len(ext_modules))
111
+ return ext_modules
112
+
113
+ except (Exception, SystemExit) as exc:
114
+ log.critical("mypyc compilation failed (%s): %s", type(exc).__name__, exc)
115
+ raise RuntimeError("mypyc compilation failed") from exc
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Main
120
+ # ---------------------------------------------------------------------------
121
+
122
+ log.info(
123
+ "Setup script invoked by %s command: %s",
124
+ "build" if IS_COMPILE_COMMAND else "metadata-only",
125
+ " ".join(sys.argv),
126
+ )
127
+ log.info(
128
+ "Setup configuration: DISABLE_MYPYC=%s, BUILD_PARALLEL=%d",
129
+ DISABLE_MYPYC,
130
+ PARALLEL,
131
+ )
132
+
133
+ ext_modules: list[Extension] = []
134
+
135
+ if IS_COMPILE_COMMAND and not DISABLE_MYPYC:
136
+ ext_modules.extend(create_mypyc_extensions())
137
+
138
+ setup(
139
+ ext_modules=ext_modules,
140
+ options={"build_ext": {"parallel": PARALLEL}},
141
+ )
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PySerials
3
- Version: 0.1.2
4
- Requires-Python: >=3.10
3
+ Version: 0.1.3
4
+ Requires-Python: >=3.12
5
5
  Requires-Dist: jsonschema<5,>=4.21.0
6
6
  Requires-Dist: referencing>=0.35.1
7
- Requires-Dist: jsonpath-ng<2,>=1.6.1
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
10
  Requires-Dist: tomlkit<0.14,>=0.11.8
@@ -1,4 +1,5 @@
1
1
  pyproject.toml
2
+ setup.py
2
3
  src/PySerials.egg-info/PKG-INFO
3
4
  src/PySerials.egg-info/SOURCES.txt
4
5
  src/PySerials.egg-info/dependency_links.txt
@@ -1,6 +1,6 @@
1
1
  jsonschema<5,>=4.21.0
2
2
  referencing>=0.35.1
3
- jsonpath-ng<2,>=1.6.1
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
6
  tomlkit<0.14,>=0.11.8
@@ -0,0 +1,38 @@
1
+ """PySerials: A Python package for reading, writing, validating, and transforming serialized data.
2
+
3
+ PySerials provides utilities for working with JSON, YAML, and TOML data formats,
4
+ including reading/writing files and strings, recursive dictionary updates,
5
+ template filling, JSON Schema validation, data comparison, and flattening of
6
+ nested structures.
7
+
8
+ Modules
9
+ -------
10
+ read
11
+ Read serialized data from files and strings.
12
+ write
13
+ Write data to serialized format strings and files.
14
+ update
15
+ Recursively update data structures and fill templates.
16
+ validate
17
+ Validate data against JSON schemas.
18
+ compare
19
+ Compare two data structures and report differences.
20
+ format
21
+ Convert data to specific TOML/YAML object types.
22
+ flatten
23
+ Flatten nested dictionaries to single-level dictionaries.
24
+ exception
25
+ Custom exception classes for error reporting.
26
+
27
+ Classes
28
+ -------
29
+ NestedDict
30
+ A dictionary wrapper supporting dot-separated key access and template filling.
31
+ PropertyDict
32
+ A dictionary wrapper supporting attribute-style access.
33
+ """
34
+
35
+ from pyserials import exception, update, validate, read, write, format, compare
36
+ from pyserials.nested_dict import NestedDict
37
+ from pyserials.property_dict import PropertyDict
38
+ from pyserials.flatten import flatten
@@ -0,0 +1,88 @@
1
+ """Compare two data structures and report differences.
2
+
3
+ This module provides functionality to recursively compare nested data structures
4
+ (dicts, lists, tuples, and scalar values), producing categorized lists of
5
+ JSONPath-like paths indicating which elements were added, removed, modified,
6
+ or left unchanged.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+
14
+ def items(
15
+ source: Any,
16
+ target: Any,
17
+ path: str = "$",
18
+ ) -> dict[str, list[str]]:
19
+ """Recursively compare two data structures and categorize differences.
20
+
21
+ Walks both ``source`` and ``target`` in parallel, emitting JSONPath-style
22
+ strings for every leaf. Paths are grouped into four categories:
23
+
24
+ * **added** – present in *source* but not in *target*.
25
+ * **removed** – present in *target* but not in *source*.
26
+ * **modified** – present in both but with different values or types.
27
+ * **unchanged** – present in both with the same value.
28
+
29
+ .. note::
30
+ Type comparison uses ``type(x) is type(y)`` (exact equality), not
31
+ ``isinstance``. Any type mismatch — including subclass vs base class
32
+ (e.g. ``True`` vs ``1``, ``OrderedDict`` vs ``dict``) — causes the
33
+ entire node to be marked **modified** with no further traversal into
34
+ its children. Two nodes are traversed recursively only when their
35
+ exact types match.
36
+
37
+ Parameters
38
+ ----------
39
+ source : Any
40
+ The *new* data structure (additions relative to *target*).
41
+ target : Any
42
+ The *old* / reference data structure.
43
+ path : str, default ``"$"``
44
+ Root JSONPath prefix for all emitted paths.
45
+
46
+ Returns
47
+ -------
48
+ dict[str, list[str]]
49
+ A mapping with keys ``"added"``, ``"removed"``, ``"modified"``,
50
+ and ``"unchanged"``, each containing a sorted list of JSONPath strings.
51
+ """
52
+
53
+ def recursive_compare(src: Any, trg: Any, curr_path: str) -> None:
54
+ if type(src) is not type(trg):
55
+ comp["modified"].append(curr_path)
56
+ return
57
+ if isinstance(src, dict):
58
+ for key in src:
59
+ if key not in trg:
60
+ comp["added"].append(f"{curr_path}.{key}")
61
+ continue
62
+ recursive_compare(src[key], trg[key], f"{curr_path}.{key}")
63
+ for key in trg:
64
+ if key not in src:
65
+ comp["removed"].append(f"{curr_path}.{key}")
66
+ return
67
+ if isinstance(src, (list, tuple)):
68
+ len_src: int = len(src)
69
+ len_trg: int = len(trg)
70
+ min_len: int = min(len_src, len_trg)
71
+ for i in range(min_len):
72
+ recursive_compare(src[i], trg[i], f"{curr_path}[{i}]")
73
+ for i in range(min_len, max(len_src, len_trg)):
74
+ comp["added" if len_src > len_trg else "removed"].append(
75
+ f"{curr_path}[{i}]"
76
+ )
77
+ return
78
+ comp["unchanged" if src == trg else "modified"].append(curr_path)
79
+ return
80
+
81
+ comp: dict[str, list[str]] = {
82
+ "added": [],
83
+ "removed": [],
84
+ "modified": [],
85
+ "unchanged": [],
86
+ }
87
+ recursive_compare(source, target, path)
88
+ return {key: sorted(comp[key]) for key in comp}
@@ -0,0 +1,8 @@
1
+ """Exceptions raised by PySerials.
2
+
3
+ All exceptions inherit from :class:`PySerialsException`, which itself
4
+ extends ``exceptionman.ReporterException`` for rich error reporting.
5
+ """
6
+
7
+ from pyserials.exception._base import PySerialsException
8
+ from pyserials.exception import read, update, validate
@@ -5,22 +5,32 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING as _TYPE_CHECKING
6
6
  from functools import partial as _partial
7
7
 
8
- from exceptionman import ReporterException as _ReporterException
9
- import mdit as _mdit
8
+ from exceptionman import ReporterException as _ReporterException # type: ignore[import-untyped]
9
+ import mdit as _mdit # type: ignore[import-untyped]
10
10
 
11
11
  if _TYPE_CHECKING:
12
12
  from mdit import Document
13
13
 
14
14
 
15
- class PySerialsException(_ReporterException):
16
- """Base class for all exceptions raised by PySerials."""
15
+ class PySerialsException(_ReporterException): # type: ignore[misc]
16
+ """Base class for all exceptions raised by PySerials.
17
17
 
18
- def __init__(self, report: Document):
19
- sphinx_config = {"html_title": "PySerials Error Report"}
18
+ Wraps an ``mdit.Document`` error report and configures Sphinx rendering.
19
+
20
+ Parameters
21
+ ----------
22
+ report : mdit.Document
23
+ The structured error report document.
24
+ """
25
+
26
+ __slots__ = ()
27
+
28
+ def __init__(self, report: Document) -> None:
29
+ sphinx_config: dict[str, str] = {"html_title": "PySerials Error Report"}
20
30
  sphinx_target_config = _mdit.target.sphinx(
21
31
  renderer=_partial(
22
32
  _mdit.render.sphinx,
23
- config=_mdit.render.get_sphinx_config(sphinx_config)
33
+ config=_mdit.render.get_sphinx_config(sphinx_config),
24
34
  )
25
35
  )
26
36
  report.target_configs["sphinx"] = sphinx_target_config
@@ -5,8 +5,9 @@ from typing import Literal as _Literal
5
5
  from pathlib import Path as _Path
6
6
 
7
7
  import ruamel.yaml as _yaml
8
+ from ruamel.yaml.error import MarkedYAMLError as _MarkedYAMLError
8
9
  import json as _json
9
- import mdit as _mdit
10
+ import mdit as _mdit # type: ignore[import-untyped]
10
11
 
11
12
  from tomlkit.exceptions import TOMLKitError as _TOMLKitError
12
13
 
@@ -26,19 +27,23 @@ class PySerialsReadException(_base.PySerialsException):
26
27
  Path to the input datafile, if data was read from a file.
27
28
  """
28
29
 
30
+ __slots__ = ("source_type", "data_type", "filepath")
31
+
29
32
  def __init__(
30
33
  self,
31
34
  source_type: _Literal["file", "string"],
32
- problem,
33
- section: dict | None = None,
35
+ problem: str | list[str] | object,
36
+ section: dict[str, object] | None = None,
34
37
  data_type: _Literal["json", "yaml", "toml"] | None = None,
35
38
  filepath: _Path | None = None,
36
- ):
39
+ ) -> None:
37
40
  intro = _mdit.inline_container(
38
41
  "Failed to read",
39
42
  f"{data_type.upper()} data" if data_type else "data",
40
43
  "from input",
41
- "string." if source_type == "string" else _mdit.inline_container(
44
+ "string."
45
+ if source_type == "string"
46
+ else _mdit.inline_container(
42
47
  "file at ", _mdit.element.code_span(filepath), "."
43
48
  ),
44
49
  separator=" ",
@@ -61,8 +66,10 @@ class PySerialsReadException(_base.PySerialsException):
61
66
  class PySerialsEmptyStringError(PySerialsReadException):
62
67
  """Exception raised when a string to be read is empty."""
63
68
 
64
- def __init__(self, data_type: _Literal["json", "yaml", "toml"]):
65
- problem = f"The string is empty."
69
+ __slots__ = ()
70
+
71
+ def __init__(self, data_type: _Literal["json", "yaml", "toml"]) -> None:
72
+ problem = "The string is empty."
66
73
  super().__init__(problem=problem, source_type="string", data_type=data_type)
67
74
  return
68
75
 
@@ -70,19 +77,21 @@ class PySerialsEmptyStringError(PySerialsReadException):
70
77
  class PySerialsInvalidFileExtensionError(PySerialsReadException):
71
78
  """Exception raised when a file to be read has an unrecognized extension."""
72
79
 
73
- def __init__(self, filepath: _Path):
80
+ __slots__ = ()
81
+
82
+ def __init__(self, filepath: _Path) -> None:
74
83
  problem = _mdit.inline_container(
75
84
  "The file extension must be one of ",
76
- _mdit.element.code_span('json'),
85
+ _mdit.element.code_span("json"),
77
86
  ", ",
78
- _mdit.element.code_span('yaml'),
87
+ _mdit.element.code_span("yaml"),
79
88
  ", ",
80
- _mdit.element.code_span('yml'),
89
+ _mdit.element.code_span("yml"),
81
90
  ", or ",
82
- _mdit.element.code_span('.toml'),
91
+ _mdit.element.code_span(".toml"),
83
92
  ", but got ",
84
- _mdit.element.code_span(str(filepath.suffix.removeprefix('.'))),
85
- ". Please provide the extension explicitly, or rename the file to have a valid extension."
93
+ _mdit.element.code_span(str(filepath.suffix.removeprefix("."))),
94
+ ". Please provide the extension explicitly, or rename the file to have a valid extension.",
86
95
  )
87
96
  super().__init__(problem=problem, source_type="file", filepath=filepath)
88
97
  return
@@ -91,18 +100,30 @@ class PySerialsInvalidFileExtensionError(PySerialsReadException):
91
100
  class PySerialsMissingFileError(PySerialsReadException):
92
101
  """Exception raised when a file to be read does not exist."""
93
102
 
94
- def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
95
- problem = f"The file does not exist."
96
- super().__init__(problem=problem, source_type="file", data_type=data_type, filepath=filepath)
103
+ __slots__ = ()
104
+
105
+ def __init__(
106
+ self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path
107
+ ) -> None:
108
+ problem: str = "The file does not exist."
109
+ super().__init__(
110
+ problem=problem, source_type="file", data_type=data_type, filepath=filepath
111
+ )
97
112
  return
98
113
 
99
114
 
100
115
  class PySerialsEmptyFileError(PySerialsReadException):
101
116
  """Exception raised when a file to be read is empty."""
102
117
 
103
- def __init__(self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path):
104
- problem = f"The file is empty."
105
- super().__init__(problem=problem, source_type="file", data_type=data_type, filepath=filepath)
118
+ __slots__ = ()
119
+
120
+ def __init__(
121
+ self, data_type: _Literal["json", "yaml", "toml"], filepath: _Path
122
+ ) -> None:
123
+ problem: str = "The file is empty."
124
+ super().__init__(
125
+ problem=problem, source_type="file", data_type=data_type, filepath=filepath
126
+ )
106
127
  return
107
128
 
108
129
 
@@ -115,6 +136,19 @@ class PySerialsInvalidDataError(PySerialsReadException):
115
136
  The input data that was supposed to be read.
116
137
  """
117
138
 
139
+ __slots__ = (
140
+ "data",
141
+ "cause",
142
+ "problem",
143
+ "problem_line",
144
+ "problem_column",
145
+ "problem_data_type",
146
+ "context",
147
+ "context_line",
148
+ "context_column",
149
+ "context_data_type",
150
+ )
151
+
118
152
  def __init__(
119
153
  self,
120
154
  source_type: _Literal["file", "string"],
@@ -122,7 +156,7 @@ class PySerialsInvalidDataError(PySerialsReadException):
122
156
  data: str,
123
157
  cause: Exception,
124
158
  filepath: _Path | None = None,
125
- ):
159
+ ) -> None:
126
160
  self.data = data
127
161
  self.cause = cause
128
162
  self.problem: str = str(cause)
@@ -135,25 +169,31 @@ class PySerialsInvalidDataError(PySerialsReadException):
135
169
  self.context_data_type: str | None = None
136
170
  self.data_type = data_type
137
171
 
138
- if isinstance(cause, _yaml.YAMLError):
172
+ if isinstance(cause, _MarkedYAMLError):
139
173
  self.problem_line = cause.problem_mark.line + 1
140
174
  self.problem_column = cause.problem_mark.column + 1
141
- self.problem_data_type = cause.problem_mark.name.removeprefix("<").removesuffix(">")
175
+ self.problem_data_type = cause.problem_mark.name.removeprefix(
176
+ "<"
177
+ ).removesuffix(">")
142
178
  self.problem = cause.problem.strip()
143
179
  if cause.context:
144
180
  self.context = cause.context.strip()
145
181
  if cause.context_mark:
146
182
  self.context_line = cause.context_mark.line + 1
147
183
  self.context_column = cause.context_mark.column + 1
148
- self.context_data_type = cause.context_mark.name.removeprefix("<").removesuffix(">")
184
+ self.context_data_type = cause.context_mark.name.removeprefix(
185
+ "<"
186
+ ).removesuffix(">")
149
187
  elif isinstance(cause, _json.JSONDecodeError):
150
188
  self.problem = cause.msg
151
189
  self.problem_line = cause.lineno
152
190
  self.problem_column = cause.colno
153
191
  elif isinstance(cause, _TOMLKitError):
154
- self.problem_line = cause.line
155
- self.problem_column = cause.col
156
- self.problem = cause.args[0].removesuffix(f" at line {self.problem_line} col {self.problem_column}")
192
+ self.problem_line = getattr(cause, "line", None)
193
+ self.problem_column = getattr(cause, "col", None)
194
+ self.problem = cause.args[0].removesuffix(
195
+ f" at line {self.problem_line} col {self.problem_column}"
196
+ )
157
197
  self.problem = self.problem.strip().capitalize().removesuffix(".")
158
198
  description = ["The data is not valid"]
159
199
  if self.problem_line:
@@ -171,34 +211,51 @@ class PySerialsInvalidDataError(PySerialsReadException):
171
211
  source_type=source_type,
172
212
  section=self._report_content(),
173
213
  data_type=data_type,
174
- filepath=filepath
214
+ filepath=filepath,
175
215
  )
176
216
  return
177
217
 
178
- def _report_content(self) -> dict:
218
+ def _report_content(self) -> dict[str, _mdit.element.Admonition]:
179
219
 
180
- def make_table(problem, line, column, data_type):
220
+ def make_table(
221
+ problem: str | None,
222
+ line: int | None,
223
+ column: int | None,
224
+ data_type: str | None,
225
+ ) -> _mdit.element.FieldList:
181
226
  items = [
182
- _mdit.element.field_list_item(title=title, body=value) for title, value in [
183
- ["Description", problem],
184
- ["Line Number", line],
185
- ["Column Number", column],
186
- ["Data Type", data_type],
187
- ] if value is not None
227
+ _mdit.element.field_list_item(title=title, body=value)
228
+ for title, value in (
229
+ ("Description", problem),
230
+ ("Line Number", line),
231
+ ("Column Number", column),
232
+ ("Data Type", data_type),
233
+ )
234
+ if value is not None
188
235
  ]
189
236
  return _mdit.element.field_list(items)
190
237
 
191
238
  content = {
192
239
  "problem_details": _mdit.element.admonition(
193
240
  title="Problem",
194
- body=make_table(self.problem, self.problem_line, self.problem_column, self.problem_data_type),
241
+ body=make_table(
242
+ self.problem,
243
+ self.problem_line,
244
+ self.problem_column,
245
+ self.problem_data_type,
246
+ ),
195
247
  type="error",
196
248
  )
197
249
  }
198
250
  if self.context:
199
251
  content["context_details"] = _mdit.element.admonition(
200
252
  title="Context",
201
- body=make_table(self.context, self.context_line, self.context_column, self.context_data_type),
253
+ body=make_table(
254
+ self.context,
255
+ self.context_line,
256
+ self.context_column,
257
+ self.context_data_type,
258
+ ),
202
259
  type="note",
203
260
  )
204
261
 
@@ -207,16 +264,22 @@ class PySerialsInvalidDataError(PySerialsReadException):
207
264
  language=self.data_type,
208
265
  caption="Data",
209
266
  line_num=True,
210
- emphasize_lines=[line for line in (self.problem_line, self.context_line) if line],
267
+ emphasize_lines=[
268
+ line for line in (self.problem_line, self.context_line) if line
269
+ ],
211
270
  degrade_to_diff=True,
212
271
  )
213
272
  content["data_full"] = (code_block_full, "full")
214
273
 
215
274
  if not (self.problem_line or self.context_line):
216
275
  code_block_short = _mdit.element.code_block(
217
- content=self.data[:1000].strip() + "\n..." if len(self.data) > 1000 else self.data,
276
+ content=self.data[:1000].strip() + "\n..."
277
+ if len(self.data) > 1000
278
+ else self.data,
218
279
  language=self.data_type,
219
- caption="Data" if len(self.data) <= 1000 else "Data (truncated to first 1000 characters)",
280
+ caption="Data"
281
+ if len(self.data) <= 1000
282
+ else "Data (truncated to first 1000 characters)",
220
283
  line_num=True,
221
284
  )
222
285
  else:
@@ -224,10 +287,12 @@ class PySerialsInvalidDataError(PySerialsReadException):
224
287
  line_start = min(self.problem_line, self.context_line)
225
288
  line_end = max(self.problem_line, self.context_line)
226
289
  else:
227
- line_start = self.problem_line or self.context_line
290
+ line_start_val = self.problem_line or self.context_line
291
+ assert line_start_val is not None
292
+ line_start = line_start_val
228
293
  line_end = line_start
229
294
  data_lines = self.data.splitlines()
230
- selected_lines = data_lines[line_start - 1:line_end]
295
+ selected_lines = data_lines[line_start - 1 : line_end]
231
296
  code_block_short = _mdit.element.code_block(
232
297
  content="\n".join(selected_lines),
233
298
  language=self.data_type,