Skip to content

Commit df864b4

Browse files
add error classes
1 parent 3f5d881 commit df864b4

File tree

4 files changed

+185
-6
lines changed

4 files changed

+185
-6
lines changed

packages/common-library/src/common_library/base.py

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from pydantic.errors import PydanticErrorMixin
2+
3+
4+
class _DefaultDict(dict):
5+
def __missing__(self, key):
6+
return f"'{key}=?'"
7+
8+
9+
class OsparcErrorMixin(PydanticErrorMixin):
10+
code: str # type: ignore[assignment]
11+
msg_template: str
12+
13+
def __new__(cls, *_args, **_kwargs):
14+
if not hasattr(cls, "code"):
15+
cls.code = cls._get_full_class_name()
16+
return super().__new__(cls)
17+
18+
def __init__(self, *_args, **kwargs) -> None:
19+
self.__dict__ = kwargs
20+
super().__init__(message=self._build_message(), code=self.code) # type: ignore[arg-type]
21+
22+
def __str__(self) -> str:
23+
return self._build_message()
24+
25+
def _build_message(self) -> str:
26+
# NOTE: safe. Does not raise KeyError
27+
return self.msg_template.format_map(_DefaultDict(**self.__dict__))
28+
29+
@classmethod
30+
def _get_full_class_name(cls) -> str:
31+
relevant_classes = [
32+
c.__name__
33+
for c in cls.__mro__[:-1]
34+
if c.__name__
35+
not in (
36+
"PydanticErrorMixin",
37+
"OsparcErrorMixin",
38+
"Exception",
39+
"BaseException",
40+
)
41+
]
42+
return ".".join(reversed(relevant_classes))

packages/common-library/tests/test_base.py

Lines changed: 0 additions & 4 deletions
This file was deleted.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# pylint: disable=protected-access
2+
# pylint: disable=redefined-outer-name
3+
# pylint: disable=unused-argument
4+
# pylint: disable=unused-variable
5+
# pylint: disable=no-member
6+
7+
8+
from datetime import datetime
9+
from typing import Any
10+
11+
import pytest
12+
from common_library.errors_classes import OsparcErrorMixin
13+
14+
15+
def test_get_full_class_name():
16+
class A(OsparcErrorMixin):
17+
...
18+
19+
class B1(A):
20+
...
21+
22+
class B2(A):
23+
...
24+
25+
class C(B2):
26+
...
27+
28+
class B12(B1, ValueError):
29+
...
30+
31+
assert B1._get_full_class_name() == "A.B1"
32+
assert C._get_full_class_name() == "A.B2.C"
33+
assert A._get_full_class_name() == "A"
34+
35+
# diamond inheritance (not usual but supported)
36+
assert B12._get_full_class_name() == "ValueError.A.B1.B12"
37+
38+
39+
def test_error_codes_and_msg_template():
40+
class MyBaseError(OsparcErrorMixin, Exception):
41+
def __init__(self, **ctx: Any) -> None:
42+
super().__init__(**ctx) # Do not forget this for base exceptions!
43+
44+
class MyValueError(MyBaseError, ValueError):
45+
msg_template = "Wrong value {value}"
46+
47+
error = MyValueError(value=42)
48+
49+
assert error.code == "ValueError.MyBaseError.MyValueError"
50+
assert f"{error}" == "Wrong value 42"
51+
52+
class MyTypeError(MyBaseError, TypeError):
53+
code = "i_want_this"
54+
msg_template = "Wrong type {type}"
55+
56+
error = MyTypeError(type="int")
57+
58+
assert error.code == "i_want_this"
59+
assert f"{error}" == "Wrong type int"
60+
61+
62+
def test_error_msg_template_override():
63+
class MyError(OsparcErrorMixin, Exception):
64+
msg_template = "Wrong value {value}"
65+
66+
error_override_msg = MyError(msg_template="I want this message")
67+
assert str(error_override_msg) == "I want this message"
68+
69+
error = MyError(value=42)
70+
assert hasattr(error, "value")
71+
assert str(error) == f"Wrong value {error.value}"
72+
73+
74+
def test_error_msg_template_nicer_override():
75+
class MyError(OsparcErrorMixin, Exception):
76+
msg_template = "Wrong value {value}"
77+
78+
def __init__(self, msg=None, **ctx: Any) -> None:
79+
super().__init__(**ctx)
80+
# positional argument msg (if defined) overrides the msg_template
81+
if msg:
82+
self.msg_template = msg
83+
84+
error_override_msg = MyError("I want this message")
85+
assert str(error_override_msg) == "I want this message"
86+
87+
error = MyError(value=42)
88+
assert hasattr(error, "value")
89+
assert str(error) == f"Wrong value {error.value}"
90+
91+
92+
def test_error_with_constructor():
93+
class MyError(OsparcErrorMixin, ValueError):
94+
msg_template = "Wrong value {value}"
95+
96+
# handy e.g. autocompletion
97+
def __init__(self, *, my_value: int = 42, **extra):
98+
super().__init__(**extra)
99+
self.value = my_value
100+
101+
error = MyError(my_value=33, something_else="yes")
102+
assert error.value == 33
103+
assert str(error) == "Wrong value 33"
104+
assert not hasattr(error, "my_value")
105+
106+
# the autocompletion does not see this
107+
assert error.something_else == "yes"
108+
109+
110+
@pytest.mark.parametrize(
111+
"str_format,ctx,expected",
112+
[
113+
pytest.param("{value:10}", {"value": "Python"}, "Python ", id="left-align"),
114+
pytest.param(
115+
"{value:>10}", {"value": "Python"}, " Python", id="right-align"
116+
),
117+
pytest.param(
118+
"{value:^10}", {"value": "Python"}, " Python ", id="center-align"
119+
),
120+
pytest.param("{v:.2f}", {"v": 3.1415926}, "3.14", id="decimals"),
121+
pytest.param(
122+
"{dt:%Y-%m-%d %H:%M}",
123+
{"dt": datetime(2020, 5, 17, 18, 45)},
124+
"2020-05-17 18:45",
125+
id="datetime",
126+
),
127+
],
128+
)
129+
def test_msg_template_with_different_formats(
130+
str_format: str, ctx: dict[str, Any], expected: str
131+
):
132+
class MyError(OsparcErrorMixin, ValueError):
133+
msg_template = str_format
134+
135+
error = MyError(**ctx)
136+
assert str(error) == expected
137+
138+
139+
def test_missing_keys_in_msg_template_does_not_raise():
140+
class MyError(OsparcErrorMixin, ValueError):
141+
msg_template = "{value} and {missing}"
142+
143+
assert str(MyError(value=42)) == "42 and 'missing=?'"

0 commit comments

Comments
 (0)