simplibs-exception 0.1.0__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.
Files changed (56) hide show
  1. simplibs/exception/__init__.py +41 -0
  2. simplibs/exception/core/__init__.py +20 -0
  3. simplibs/exception/core/_internal_exception/SimpleExceptionInternalError.py +91 -0
  4. simplibs/exception/core/_internal_exception/__init__.py +16 -0
  5. simplibs/exception/core/data/SimpleExceptionData.py +114 -0
  6. simplibs/exception/core/data/__init__.py +15 -0
  7. simplibs/exception/exception/SimpleException.py +220 -0
  8. simplibs/exception/exception/__init__.py +27 -0
  9. simplibs/exception/exception/_mixins/__init__.py +43 -0
  10. simplibs/exception/exception/_mixins/dunders/InitSubclass.py +60 -0
  11. simplibs/exception/exception/_mixins/dunders/New.py +136 -0
  12. simplibs/exception/exception/_mixins/dunders/__init__.py +15 -0
  13. simplibs/exception/exception/_mixins/dunders/_utils/__init__.py +14 -0
  14. simplibs/exception/exception/_mixins/dunders/_utils/check_children_class_attributes.py +143 -0
  15. simplibs/exception/exception/_mixins/normalizers/NormalizeParam.py +61 -0
  16. simplibs/exception/exception/_mixins/normalizers/ProcessExceptionParam.py +85 -0
  17. simplibs/exception/exception/_mixins/normalizers/ProcessGetLocationParam.py +57 -0
  18. simplibs/exception/exception/_mixins/normalizers/ProcessHowToFixParam.py +80 -0
  19. simplibs/exception/exception/_mixins/normalizers/ProcessSkipLocationsParam.py +67 -0
  20. simplibs/exception/exception/_mixins/normalizers/__init__.py +23 -0
  21. simplibs/exception/exception/_mixins/serializers/ToDebugDict.py +88 -0
  22. simplibs/exception/exception/_mixins/serializers/ToDict.py +52 -0
  23. simplibs/exception/exception/_mixins/serializers/ToJson.py +38 -0
  24. simplibs/exception/exception/_mixins/serializers/__init__.py +17 -0
  25. simplibs/exception/modes/LOG.py +105 -0
  26. simplibs/exception/modes/ONELINE.py +94 -0
  27. simplibs/exception/modes/PRETTY.py +150 -0
  28. simplibs/exception/modes/SIMPLE.py +105 -0
  29. simplibs/exception/modes/__init__.py +32 -0
  30. simplibs/exception/modes/base_class/ModeBase.py +192 -0
  31. simplibs/exception/modes/base_class/__init__.py +13 -0
  32. simplibs/exception/modes/base_class/_mixins/PrintCallerInfo.py +84 -0
  33. simplibs/exception/modes/base_class/_mixins/PrintIntroLine.py +46 -0
  34. simplibs/exception/modes/base_class/_mixins/PrintValueWithType.py +56 -0
  35. simplibs/exception/modes/base_class/_mixins/__init__.py +18 -0
  36. simplibs/exception/modes/base_class/_validations/SimpleExceptionModeError.py +30 -0
  37. simplibs/exception/modes/base_class/_validations/__init__.py +17 -0
  38. simplibs/exception/modes/base_class/_validations/validate_has_simple_exception_data.py +44 -0
  39. simplibs/exception/settings/SimpleExceptionSettings.py +115 -0
  40. simplibs/exception/settings/__init__.py +23 -0
  41. simplibs/exception/settings/_meta/SimpleExceptionSettingsMeta.py +76 -0
  42. simplibs/exception/settings/_meta/__init__.py +16 -0
  43. simplibs/exception/settings/_meta/validations/SimpleExceptionSettingsError.py +30 -0
  44. simplibs/exception/settings/_meta/validations/__init__.py +22 -0
  45. simplibs/exception/settings/_meta/validations/validate_dynamic_cls_cache.py +33 -0
  46. simplibs/exception/settings/_meta/validations/validate_get_location.py +26 -0
  47. simplibs/exception/settings/_meta/validations/validate_location_blacklist.py +48 -0
  48. simplibs/exception/settings/_meta/validations/validate_message_mode.py +28 -0
  49. simplibs/exception/utils/__init__.py +22 -0
  50. simplibs/exception/utils/bool_or_exception.py +66 -0
  51. simplibs/exception/utils/extract_caller_info.py +130 -0
  52. simplibs_exception-0.1.0.dist-info/METADATA +506 -0
  53. simplibs_exception-0.1.0.dist-info/RECORD +56 -0
  54. simplibs_exception-0.1.0.dist-info/WHEEL +5 -0
  55. simplibs_exception-0.1.0.dist-info/licenses/LICENSE +21 -0
  56. simplibs_exception-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,41 @@
1
+ from .exception import SimpleException
2
+ from .utils import bool_or_exception, extract_caller_info
3
+ from .modes import PRETTY, SIMPLE, ONELINE, LOG, ModeBase
4
+ from .settings import SimpleExceptionSettings, SimpleExceptionSettingsError
5
+
6
+
7
+ __all__ = [
8
+ # Core class
9
+ "SimpleException",
10
+ # Utils
11
+ "bool_or_exception",
12
+ "extract_caller_info",
13
+ # Modes
14
+ "PRETTY",
15
+ "SIMPLE",
16
+ "ONELINE",
17
+ "LOG",
18
+ "ModeBase",
19
+ # Settings
20
+ "SimpleExceptionSettings",
21
+ "SimpleExceptionSettingsError",
22
+ ]
23
+
24
+
25
+ _DESIGN_NOTES = """
26
+ # simple_exception
27
+
28
+ ## Public API
29
+ | Name | Description |
30
+ |--------------------------------|-------------------------------------------------------------------|
31
+ | `SimpleException` | Core class — the foundation for all exceptions in the ecosystem |
32
+ | `bool_or_exception` | Shortcut for conditional exception raising |
33
+ | `extract_caller_info` | Utility for retrieving call site information from the stack |
34
+ | `PRETTY` | Default mode — structured output with separator lines |
35
+ | `SIMPLE` | Plain text output without decorations |
36
+ | `ONELINE` | Compact single-line output |
37
+ | `LOG` | Key=value format for log parsers |
38
+ | `ModeBase` | Base class for custom modes |
39
+ | `SimpleExceptionSettings` | Central configuration of the library |
40
+ | `SimpleExceptionSettingsError` | Exception for errors when changing settings |
41
+ """
@@ -0,0 +1,20 @@
1
+ from .data import SimpleExceptionData
2
+ from ._internal_exception import SimpleExceptionInternalError
3
+
4
+
5
+ _DESIGN_NOTES = """
6
+ # core
7
+
8
+ ## Contents
9
+ The data layer of the library — defines the exception structure and the base
10
+ internal exception that consumes this structure.
11
+
12
+ | Name | Description |
13
+ |--------------------------------|-----------------------------------------------------------|
14
+ | `SimpleExceptionData` | Data class — exception structure and default values |
15
+ | `SimpleExceptionInternalError` | Base internal exception — foundation for grouped exceptions|
16
+
17
+ ## Note
18
+ This package has no dependencies on the rest of the library — it is completely
19
+ isolated. Everything else draws from it; nothing points back into it.
20
+ """
@@ -0,0 +1,91 @@
1
+ from dataclasses import dataclass
2
+ # Commons
3
+ from ..data import SimpleExceptionData
4
+
5
+
6
+ @dataclass
7
+ class SimpleExceptionInternalError(SimpleExceptionData, Exception):
8
+ """Internal library exception — no validation, direct output only."""
9
+
10
+
11
+ # Available attributes:
12
+ error_name: str = "INTERNAL ERROR"
13
+ # value: object = UNSET
14
+ # value_label: str = UNSET
15
+ # expected: str = UNSET
16
+ # problem: str = UNSET
17
+ # context: str = UNSET
18
+ # how_to_fix: tuple[str, ...] = UNSET
19
+
20
+
21
+ # Build the message:
22
+ def __post_init__(self):
23
+
24
+ # 1. Render the message
25
+ from ...modes import PRETTY
26
+ rendered_message = PRETTY(self, validate=False)
27
+
28
+ # 2. Pass the message to the exception
29
+ # Instead of super().__init__ we write directly to Exception.args —
30
+ # this ensures Exception(message) works correctly without overwriting our data.
31
+ Exception.__init__(self, rendered_message)
32
+
33
+
34
+ _DESIGN_NOTES = """
35
+ # SimpleExceptionInternalError
36
+
37
+ ## Purpose
38
+ The base internal exception of the library — completely isolated from
39
+ `SimpleException` logic. Inherits directly from `SimpleExceptionData`
40
+ and `Exception`, with no dependency on the rest of the library beyond
41
+ the `PRETTY` mode.
42
+
43
+ ## Isolation and lazy import
44
+ `PRETTY` is loaded lazily inside `__post_init__` to avoid a circular import —
45
+ `SimpleExceptionInternalError` lives in `core/_internal_exception` and a
46
+ standard top-level import of `modes` would pull it back in. The lazy import
47
+ breaks this cycle and loads the mode only when it is actually needed.
48
+
49
+ `PRETTY` is called with `validate=False` for two reasons:
50
+ - This is an internal call where the data is already guaranteed to be correct —
51
+ validation is unnecessary.
52
+ - Validation would re-import `modes` and cause a circular import even through
53
+ the lazy import.
54
+
55
+ `PRETTY` is hardcoded — a deliberate choice for reliability. Internal library
56
+ exceptions must produce consistent output regardless of the active settings
57
+ configuration. In the very situations these exceptions handle (misconfiguration,
58
+ logic errors in modes) the settings may be unreliable or only partially
59
+ initialised.
60
+
61
+ ## Group base class
62
+ Serves as the base for grouped internal library exceptions:
63
+ ```python
64
+ class SimpleExceptionSettingsError(SimpleExceptionInternalError):
65
+ error_name: str = "SETTINGS ERROR"
66
+ ```
67
+ Grouped exceptions can be caught together:
68
+ ```python
69
+ except SimpleExceptionInternalError:
70
+ ...
71
+ ```
72
+
73
+ ## Attributes to define
74
+ A full description of all attributes is in `SimpleExceptionData._DESIGN_NOTES`.
75
+ For internal exceptions, only the following are typically needed:
76
+
77
+ | Attribute | Description |
78
+ |---------------|-------------------------------------------------------------------|
79
+ | `error_name` | Exception group name — displayed as the heading in the output |
80
+ | `value` | The value that caused the exception |
81
+ | `value_label` | Label for the value — e.g. the attribute or parameter name |
82
+ | `expected` | What was expected |
83
+ | `problem` | What is wrong |
84
+ | `context` | Additional context — only include if it adds meaningful information|
85
+ | `how_to_fix` | Tips on how to resolve the error |
86
+
87
+ ## Commented-out attributes
88
+ The commented-out attributes in the class definition serve as a quick
89
+ declarative reference — without reading the documentation it is immediately
90
+ clear which attributes are meaningful to define.
91
+ """
@@ -0,0 +1,16 @@
1
+ from .SimpleExceptionInternalError import SimpleExceptionInternalError
2
+
3
+
4
+ _DESIGN_NOTES = """
5
+ # core/_internal_exception
6
+
7
+ ## Contents
8
+ The base internal exception of the library — placed here because it inherits
9
+ directly from `SimpleExceptionData` and is therefore a natural part of the
10
+ data layer. Serves as the foundation for all grouped internal exceptions
11
+ in the library.
12
+
13
+ | Name | Description |
14
+ |--------------------------------|-------------------------------------------------------|
15
+ | `SimpleExceptionInternalError` | Base internal exception — foundation for other groups |
16
+ """
@@ -0,0 +1,114 @@
1
+ from dataclasses import dataclass
2
+ from simplibs.sentinels import UNSET, UnsetType
3
+
4
+
5
+ @dataclass
6
+ class SimpleExceptionData:
7
+ """Data class defining the structure and default values of SimpleException."""
8
+
9
+ # --- Core exception info ---
10
+ error_name: str = "ERROR"
11
+ exception: type[Exception] | UnsetType = UNSET
12
+ _intercepted_exception: str | UnsetType = UNSET
13
+
14
+ # --- Info about the inspected value ---
15
+ value: object = UNSET
16
+ value_label: str | UnsetType = UNSET
17
+
18
+ # --- Exception description ---
19
+ expected: str | UnsetType = UNSET
20
+ problem: str | UnsetType = UNSET
21
+ context: str | UnsetType = UNSET
22
+ message: str | UnsetType = UNSET
23
+
24
+ # --- How to fix ---
25
+ how_to_fix: tuple[str, ...] | UnsetType = UNSET
26
+
27
+ # --- Location info ---
28
+ _get_location: int | bool = True
29
+ _skip_locations: tuple[str, ...] = ()
30
+
31
+ # --- Single-line output ---
32
+ _oneline: bool = False
33
+
34
+
35
+ _DESIGN_NOTES = """
36
+ # SimpleExceptionData
37
+
38
+ ## Purpose
39
+ A data class defining the structure, default values, and interface of `SimpleException`.
40
+ Completely isolated from the rest of the library — it has no dependencies on
41
+ `SimpleExceptionSettings` or any other library class. This makes it a shared
42
+ foundation for:
43
+
44
+ 1. **`SimpleException`** — subclasses can override public attributes
45
+ to change default behaviour.
46
+ 2. **Internal exceptions** — `SimpleExceptionInternalError` and its subclasses
47
+ share the same structure without depending on `SimpleException` logic.
48
+ 3. **Data protocol for modes** — `ModeBase` expects data with this structure.
49
+ Custom modes can optionally validate input via `validate=True`.
50
+
51
+ ## Underscore convention
52
+ - **Without underscore** — input parameters that the user can set either as
53
+ `__init__` arguments or as class-level attributes on subclasses.
54
+ - **With underscore** (`_intercepted_exception`, `_get_location`, etc.) —
55
+ values that are computed or managed automatically by the system.
56
+ The user should never set these directly.
57
+
58
+ ## Attribute reference
59
+
60
+ ### Core info
61
+ | Attribute | Default | Description |
62
+ |-------------------------|-----------|------------------------------------------------------|
63
+ | `error_name` | `"ERROR"` | Error name displayed in the exception output |
64
+ | `exception` | `UNSET` | Exception class dynamically added to the MRO |
65
+ | `_intercepted_exception`| `UNSET` | Description of a caught exception — set automatically|
66
+
67
+ ### Inspected value
68
+ | Attribute | Default | Description |
69
+ |---------------|---------|-----------------------------------------------------------|
70
+ | `value` | `UNSET` | The value that caused the exception |
71
+ | `value_label` | `UNSET` | Human-readable label for the value (e.g. `"parameter age"`) |
72
+
73
+ ### Exception description
74
+ | Attribute | Default | Description |
75
+ |------------|---------|------------------------------------------------------------------|
76
+ | `expected` | `UNSET` | What was expected (e.g. `"a positive integer"`) |
77
+ | `problem` | `UNSET` | What is wrong (e.g. `"value is negative"`) |
78
+ | `context` | `UNSET` | Broader context — only include if it adds meaningful information |
79
+ | `message` | `UNSET` | Free-form message — an alternative to the structured parameters |
80
+
81
+ ### Location
82
+ | Attribute | Default | Description |
83
+ |-------------------|---------|---------------------------------------------------------------------|
84
+ | `_get_location` | `True` | Enable/disable location reporting, or set stack depth |
85
+ | `_skip_locations` | `()` | Strings matched against file paths — a match causes the frame to be skipped |
86
+
87
+ ### How to fix
88
+ | Attribute | Default | Description |
89
+ |--------------|---------|---------------------------------------------------------------|
90
+ | `how_to_fix` | `UNSET` | Tips on how to resolve the error — displayed in the output |
91
+
92
+ ### Output format
93
+ | Attribute | Default | Description |
94
+ |------------|---------|--------------------------------------------------------------------------|
95
+ | `_oneline` | `False` | When `True`, overrides the active mode and prints the exception on a single line. Useful when a specific call site needs compact output regardless of the configured default mode. |
96
+
97
+ ## Overriding defaults on a subclass
98
+ ```python
99
+ class MyError(SimpleException):
100
+ error_name = "MY_ERROR"
101
+ expected = "a positive integer"
102
+ how_to_fix = "Provide a value greater than 0."
103
+ ```
104
+
105
+ ## Notes
106
+ - The class itself contains no logic — it is purely declarative.
107
+ All logic lives in the mixins and in `SimpleException.__init__`.
108
+ - It serves as the data protocol for `ModeBase.render_message` — the data must
109
+ contain all attributes defined here (they may hold `UNSET`), otherwise
110
+ validation will fail. Internal calls always pass `validate=False`.
111
+ - Internal exceptions (`SimpleExceptionInternalError` and its subclasses)
112
+ inherit directly from this class — bypassing `SimpleException` logic
113
+ and avoiding any circular dependency.
114
+ """
@@ -0,0 +1,15 @@
1
+ from .SimpleExceptionData import SimpleExceptionData
2
+
3
+
4
+ _DESIGN_NOTES = """
5
+ # core/data
6
+
7
+ ## Contents
8
+ The data layer of the library — contains the base structures defining the
9
+ shape of `SimpleException`. These classes contain no logic and serve as a
10
+ pure data contract between the individual parts of the system.
11
+
12
+ | Name | Description |
13
+ |-----------------------|-----------------------------------------------------------|
14
+ | `SimpleExceptionData` | Data class defining the structure and default values |
15
+ """
@@ -0,0 +1,220 @@
1
+ from simplibs.sentinels import UNSET, UnsetType
2
+ # Commons
3
+ from ..core import SimpleExceptionData
4
+ from ..settings import SimpleExceptionSettings as S
5
+ # Inners
6
+ from ._mixins.dunders import DunderInitSubclassMixin, DunderNewMixin
7
+ from ._mixins.serializers import ToDictMixin, ToDebugDictMixin, ToJsonMixin
8
+ from ._mixins.normalizers import (
9
+ NormalizeParamMixin,
10
+ ProcessExceptionParamMixin,
11
+ ProcessHowToFixParamMixin,
12
+ ProcessSkipLocationsParamMixin,
13
+ ProcessGetLocationParamMixin
14
+ )
15
+
16
+
17
+ class SimpleException(
18
+ # Base class
19
+ SimpleExceptionData, # Base class with class-level attributes
20
+ # Dunders
21
+ DunderInitSubclassMixin, # def __init_subclass__(cls, **kwargs) -> None
22
+ DunderNewMixin, # def __new__(cls, *args, exception: type[Exception] | UnsetType, **kwargs) -> None
23
+ # Normalizers
24
+ NormalizeParamMixin, # def _normalize_param(self, value: Any, attr: str, typ: type) -> Any
25
+ ProcessExceptionParamMixin, # def _process_exception_param(self, value: type[Exception]) -> tuple[type[Exception] | UnsetType, str | UnsetType]
26
+ ProcessHowToFixParamMixin, # def _process_how_to_fix_param(self, value: tuple[str, ...] | str | UnsetType) -> tuple[str, ...] | UnsetType
27
+ ProcessSkipLocationsParamMixin, # def _process_skip_locations_param(self, value: tuple[str, ...] | list[str] | str | UnsetType) -> tuple[str, ...]
28
+ ProcessGetLocationParamMixin, # def _process_get_location_param(self, value: int | bool | UnsetType) -> int | bool
29
+ # Serializers
30
+ ToDictMixin, # def to_dict(self) -> dict
31
+ ToDebugDictMixin, # def to_debug_dict(self) -> dict
32
+ ToJsonMixin, # def to_json(self) -> str
33
+ # Base exceptions
34
+ Exception # Base exception enabling the raise mechanism
35
+ ):
36
+ """Structured exception for the Simple ecosystem."""
37
+
38
+
39
+ # -------------------------------------------------------------------------
40
+ # __init__ — attribute assignment and message assembly
41
+ # -------------------------------------------------------------------------
42
+
43
+ def __init__(
44
+ self,
45
+ message: str | UnsetType = UNSET,
46
+ *,
47
+ value: object = UNSET,
48
+ value_label: str | UnsetType = UNSET,
49
+ expected: str | UnsetType = UNSET,
50
+ problem: str | UnsetType = UNSET,
51
+ context: str | UnsetType = UNSET,
52
+ how_to_fix: tuple[str, ...] | str | UnsetType = UNSET,
53
+ error_name: str | UnsetType = UNSET,
54
+ exception: Exception | type[Exception] | UnsetType = UNSET,
55
+ get_location: bool | int | UnsetType = UNSET,
56
+ skip_locations: tuple[str, ...] | str | UnsetType = UNSET,
57
+ oneline: bool = False,
58
+ ):
59
+
60
+ # --- Core info ---
61
+ self.error_name = self._normalize_param(error_name, "error_name", str)
62
+ self.exception, self._intercepted_exception = self._process_exception_param(exception)
63
+
64
+ # --- Inspected value ---
65
+ self.value = value
66
+ self.value_label = self._normalize_param(value_label, "value_label", str)
67
+
68
+ # --- Exception description ---
69
+ self.expected = self._normalize_param(expected, "expected", str)
70
+ self.problem = self._normalize_param(problem, "problem", str)
71
+ self.context = self._normalize_param(context, "context", str)
72
+ self.message = self._normalize_param(message, "message", str)
73
+
74
+ # --- How to fix ---
75
+ self.how_to_fix = self._process_how_to_fix_param(how_to_fix)
76
+
77
+ # --- Location ---
78
+ self._get_location = self._process_get_location_param(get_location)
79
+ self._skip_locations = self._process_skip_locations_param(skip_locations)
80
+
81
+ # --- Single-line output ---
82
+ self._oneline = self._normalize_param(oneline, "oneline", bool)
83
+
84
+ # --- Assemble the message ---
85
+ self._rendered_message = self._render_message()
86
+
87
+ # --- Initialise Exception — bypasses the dataclass __init__ in the MRO ---
88
+ Exception.__init__(self, self._rendered_message)
89
+
90
+
91
+ # -------------------------------------------------------------------------
92
+ # Message assembly — delegates to the output mode
93
+ # -------------------------------------------------------------------------
94
+
95
+ def _render_message(self) -> str:
96
+ """Passes the data to the active mode and returns the assembled string."""
97
+ if self._oneline:
98
+ from ..modes import ONELINE
99
+ return ONELINE(self, validate=False)
100
+ return S.DEFAULT_MESSAGE_MODE(self, validate=False)
101
+
102
+
103
+ # -------------------------------------------------------------------------
104
+ # Dunder methods
105
+ # -------------------------------------------------------------------------
106
+
107
+ def __repr__(self) -> str:
108
+ return f"<{self.__class__.__name__}(error_name={self.error_name!r}, value={self.value!r})>"
109
+
110
+ def __str__(self) -> str:
111
+ return self._rendered_message
112
+
113
+
114
+ _DESIGN_NOTES = """
115
+ # SimpleException
116
+
117
+ ## Purpose
118
+ The core class of the ecosystem — a structured exception that combines a data
119
+ layer, validation, normalisation, and output modes into a single unit. Designed
120
+ so that the exception itself communicates with the developer — describing the
121
+ cause, the circumstances, and the path to a fix.
122
+
123
+ ## Class composition
124
+ `SimpleException` is assembled from mixins, each with a single responsibility:
125
+
126
+ ### Data layer
127
+ - `SimpleExceptionData` — class-level attributes and default values, the source
128
+ of truth for all parameters. Subclasses can override them.
129
+
130
+ ### Dunders
131
+ - `DunderInitSubclassMixin` — validates subclasses at definition time,
132
+ catching typos and incorrect types immediately on import.
133
+ - `DunderNewMixin` — dynamically adds `exception` to the instance ancestors,
134
+ enabling `isinstance(e, ValueError)` without static inheritance.
135
+
136
+ ### Normalizers
137
+ - `NormalizeParamMixin` — normalises simple parameters based on a type check.
138
+ If the value does not match the expected type, the class-level default is
139
+ returned. Never raises an exception.
140
+ - `ProcessExceptionParamMixin` — processes the `exception` parameter,
141
+ accepting both an exception class and an exception instance. Handles the
142
+ fallback to the class-level default internally when no value is provided —
143
+ consistent with the other normalisation methods.
144
+ - `ProcessHowToFixParamMixin` — normalises the `how_to_fix` parameter,
145
+ accepting `str`, `tuple[str, ...]`, or `list[str]` and normalising to
146
+ `tuple[str, ...]`. Falls back to the class-level default.
147
+ - `ProcessGetLocationParamMixin` — processes the `get_location` parameter,
148
+ returning the provided value or `S.DEFAULT_GET_LOCATION` from settings.
149
+ Unlike other normalisations, the fallback comes from settings rather than
150
+ the class-level default — `get_location` is a global library setting.
151
+ - `ProcessSkipLocationsParamMixin` — processes the `skip_locations` parameter,
152
+ normalises the input to `tuple[str, ...]` and merges it with
153
+ `S.DEFAULT_LOCATION_BLACKLIST`. The merge happens inside the method —
154
+ the user's blacklist and the global blacklist always apply together.
155
+
156
+ ### Serializers
157
+ - `ToDictMixin` — public attributes as a dictionary, UNSET values omitted.
158
+ - `ToDebugDictMixin` — public and private computed attributes as a dictionary.
159
+ - `ToJsonMixin` — public attributes as a JSON string.
160
+
161
+ ## Private vs public attributes
162
+ Attributes are divided by their semantics:
163
+
164
+ **Public** — exception data, included in `to_dict()`:
165
+ error_name, exception, value, value_label,
166
+ expected, problem, context, message, how_to_fix
167
+
168
+ **Private** — behavioural configuration and computed values, only in `to_debug_dict()`:
169
+ _get_location, _skip_locations, _oneline,
170
+ _intercepted_exception, _rendered_message
171
+
172
+ ## Output modes
173
+ The message is assembled in `_render_message()`, which delegates to a mode:
174
+ - `_oneline=True` — uses the `ONELINE` mode (lazy import)
175
+ - otherwise — uses `S.DEFAULT_MESSAGE_MODE` (default: `PRETTY`)
176
+
177
+ The lazy import of `ONELINE` in `_render_message` is intentional — it prevents
178
+ a circular dependency between the `exception` and `modes` modules.
179
+
180
+ ## Processing flow in `__init__`
181
+ 1. Normalise core info (error_name, exception)
182
+ 2. Normalise the inspected value (value, value_label)
183
+ 3. Normalise the exception description (expected, problem, context, message)
184
+ 4. Process how_to_fix
185
+ 5. Process location (_get_location, _skip_locations)
186
+ 6. Set the output format (_oneline)
187
+ 7. Assemble the message via _render_message()
188
+ 8. Pass the message to Exception.__init__()
189
+
190
+ ## How to create a custom exception
191
+ class MyValidationError(SimpleException):
192
+ error_name = "VALIDATION ERROR"
193
+ expected = "a positive integer"
194
+ how_to_fix = (
195
+ "Provide a value greater than 0.",
196
+ "Use the int type.",
197
+ )
198
+
199
+ raise MyValidationError(value=age, value_label="parameter age")
200
+
201
+ Class-level attributes overridden on the subclass take precedence over
202
+ parameters — they can always be overridden again at the call site.
203
+
204
+ ## Using the exception parameter
205
+ # As a class:
206
+ raise SimpleException(exception=ValueError, problem="negative value")
207
+
208
+ # As an instance from an except block:
209
+ try:
210
+ ...
211
+ except ValueError as e:
212
+ raise SimpleException(exception=e, problem="negative value")
213
+
214
+ ## Notes
215
+ - `_render_message` stores the result in `self._rendered_message` — a private
216
+ attribute that `__str__` returns directly. `self.message` remains a clean
217
+ input parameter accessible via `to_dict()`.
218
+ - All normalisation methods are designed to never raise an exception —
219
+ in the worst case they return the class-level default or the settings value.
220
+ """
@@ -0,0 +1,27 @@
1
+ from .SimpleException import SimpleException
2
+
3
+
4
+ _DESIGN_NOTES = """
5
+ # exception
6
+
7
+ ## Contents
8
+ The core class of the ecosystem — a structured exception combining the data
9
+ layer, validation, normalisation, and output modes.
10
+
11
+ | Name | Description |
12
+ |------------------|------------------------------------------------------------------|
13
+ | `SimpleException`| Core class — the foundation for all exceptions in the Simple ecosystem |
14
+
15
+ ## Usage
16
+ ```python
17
+ from simple_exception.exception import SimpleException
18
+
19
+ raise SimpleException(
20
+ value_label = "parameter age",
21
+ expected = "a positive integer",
22
+ value = age,
23
+ problem = "value is negative",
24
+ how_to_fix = "Provide a value greater than 0.",
25
+ )
26
+ ```
27
+ """
@@ -0,0 +1,43 @@
1
+ # Dunders
2
+ from .dunders import DunderInitSubclassMixin, DunderNewMixin
3
+ # Normalizers
4
+ from .normalizers import (
5
+ NormalizeParamMixin,
6
+ ProcessExceptionParamMixin,
7
+ ProcessHowToFixParamMixin,
8
+ ProcessGetLocationParamMixin,
9
+ ProcessSkipLocationsParamMixin,
10
+ )
11
+ # Serializers
12
+ from .serializers import ToDictMixin, ToDebugDictMixin, ToJsonMixin
13
+
14
+
15
+ _DESIGN_NOTES = """
16
+ # exception/_mixins
17
+
18
+ ## Contents
19
+ The aggregation point for all mixins of the `SimpleException` class.
20
+ Imports from subpackages and re-exports them as a unified interface.
21
+
22
+ | Package | Mixins |
23
+ |---------------|------------------------------------------------------------------------|
24
+ | `dunders` | DunderInitSubclassMixin, DunderNewMixin |
25
+ | `normalizers` | NormalizeParamMixin, ProcessExceptionParamMixin, |
26
+ | | ProcessHowToFixParamMixin, ProcessGetLocationParamMixin, |
27
+ | | ProcessSkipLocationsParamMixin |
28
+ | `serializers` | ToDictMixin, ToDebugDictMixin, ToJsonMixin |
29
+
30
+ ## Usage
31
+ from ._mixins import (
32
+ DunderInitSubclassMixin,
33
+ DunderNewMixin,
34
+ NormalizeParamMixin,
35
+ ProcessExceptionParamMixin,
36
+ ProcessHowToFixParamMixin,
37
+ ProcessGetLocationParamMixin,
38
+ ProcessSkipLocationsParamMixin,
39
+ ToDictMixin,
40
+ ToDebugDictMixin,
41
+ ToJsonMixin,
42
+ )
43
+ """
@@ -0,0 +1,60 @@
1
+ # Commons
2
+ from ....core import SimpleExceptionData
3
+ # Inners
4
+ from ._utils import check_children_class_attributes
5
+
6
+
7
+ class DunderInitSubclassMixin:
8
+ """Mixin for validating subclasses at definition time."""
9
+
10
+ def __init_subclass__(
11
+ cls,
12
+ **kwargs
13
+ ) -> None:
14
+ """
15
+ Validates the subclass when it is defined — checks for typos and incorrect attribute types.
16
+
17
+ Args:
18
+ cls: The newly defined class (subclass).
19
+ """
20
+ super().__init_subclass__(**kwargs)
21
+ check_children_class_attributes(SimpleExceptionData, cls)
22
+
23
+
24
+ _DESIGN_NOTES = """
25
+ # DunderInitSubclassMixin
26
+
27
+ ## Purpose
28
+ Validates `SimpleException` subclasses at **definition time** — that is, at
29
+ import, not when an instance is created. Developer errors (typos, incorrect
30
+ attribute types) are surfaced immediately.
31
+
32
+ ## Why at class definition time rather than at instantiation
33
+ `__init_subclass__` is called automatically by Python the moment the
34
+ interpreter processes the subclass definition. This means the developer
35
+ receives an error immediately on module import — not somewhere during
36
+ program execution where the root cause would be harder to trace.
37
+
38
+ ## What is checked
39
+ Delegates to `check_children_class_attributes`, which verifies:
40
+ - The subclass contains no attributes not defined in `SimpleExceptionData`
41
+ (likely typos)
42
+ - Attribute values match the types declared in `SimpleExceptionData`
43
+
44
+ ## Why the logic lives in a separate function
45
+ `__init_subclass__` decides **when** to validate.
46
+ `check_children_class_attributes` decides **how** to validate.
47
+ Separation of concerns — the mixin remains a clean orchestrator that is
48
+ readable at a glance. The validation logic itself (~35 lines with two
49
+ branches and walrus operators) would clutter the method to the point where
50
+ the intent would no longer be obvious without reading the details.
51
+
52
+ ## Notes
53
+ - `super().__init_subclass__(**kwargs)` must always be called first —
54
+ this ensures correct MRO behaviour under multiple inheritance.
55
+ - The private implementation detail `check_children_class_attributes` lives
56
+ in the `_utils` subdirectory alongside this mixin — it does not leave
57
+ that scope.
58
+ - Errors are reported via `SimpleExceptionInternalError` — an internal
59
+ library exception that signals a developer error, not a runtime error.
60
+ """