StructResult 0.8.2__tar.gz → 0.8.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: StructResult
3
- Version: 0.8.2
3
+ Version: 0.8.4
4
4
  Summary: structural result with ExceptionGroup
5
5
  Author-email: Serj Kotilevski <youserj@outlook.com>
6
6
  Project-URL: Source, https://github.com/youserj/Result_prj
@@ -9,7 +9,7 @@ where = ["src"]
9
9
 
10
10
  [project]
11
11
  name = "StructResult"
12
- version = "0.8.2"
12
+ version = "0.8.4"
13
13
  requires-python = ">= 3.12"
14
14
  authors = [
15
15
  {name="Serj Kotilevski", email="youserj@outlook.com"}
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Optional, Self, Protocol, Iterator, Any
2
+ from typing import Optional, Self, Protocol, Iterator, Any, Never
3
3
 
4
4
  """
5
5
  Functional error handling system with:
@@ -19,6 +19,9 @@ class Result(Protocol):
19
19
  def is_ok(self) -> bool:
20
20
  """Returns True if successful (no errors)"""
21
21
 
22
+ def unwrap(self) -> Any:
23
+ """Returns value or raises exception if errors exist"""
24
+
22
25
 
23
26
  class Ok(Result):
24
27
  """Singleton success marker without value"""
@@ -28,6 +31,14 @@ class Ok(Result):
28
31
  def __str__(self) -> str:
29
32
  return "OK"
30
33
 
34
+ @property
35
+ def value(self) -> "Ok":
36
+ return OK
37
+
38
+ def unwrap(self) -> "Ok":
39
+ """Always return OK"""
40
+ return OK
41
+
31
42
 
32
43
  OK = Ok()
33
44
 
@@ -35,35 +46,32 @@ OK = Ok()
35
46
  class ErrorPropagator(Result, Protocol):
36
47
  """Protocol for error-accumulating types"""
37
48
  err: Optional[ExceptionGroup]
38
- msg: str
39
49
 
40
50
  def is_ok(self) -> bool:
41
51
  return self.err is None
42
52
 
43
- def append_err(self, e: Exception | ExceptionGroup) -> Self:
53
+ def append_e(self, e: Exception, msg: str = "") -> Self:
54
+ """adds to existing group or creates new one"""
55
+ if self.err is None:
56
+ self.err = ExceptionGroup(msg, (e,))
57
+ elif msg == self.err.message:
58
+ self.err = ExceptionGroup(msg, (*self.err.exceptions, e))
59
+ else:
60
+ self.err = ExceptionGroup(msg, (e, self.err))
61
+ return self
62
+
63
+ def append_err(self, err: ExceptionGroup) -> Self:
44
64
  """Adds an exception or exception group to the collector.
45
65
  Rules:
46
66
  - For ExceptionGroup with matching message: merges exceptions
47
67
  - For ExceptionGroup with different message: preserves structure
48
- - For single Exception: adds to existing group or creates new one
49
68
  """
50
- if isinstance(e, ExceptionGroup):
51
- if self.err is None:
52
- if self.msg == e.message:
53
- self.err = e
54
- else:
55
- self.err = ExceptionGroup(self.msg, (e,))
56
- elif self.msg == e.message:
57
- self.err = ExceptionGroup(self.msg, (*self.err.exceptions, *e.exceptions))
58
- else:
59
- self.err = ExceptionGroup(self.msg, (*self.err.exceptions, e))
60
- else: # for Exception
61
- if self.err is None:
62
- self.err = ExceptionGroup(self.msg, (e,))
63
- elif self.msg == self.err.message:
64
- self.err = ExceptionGroup(self.msg, (*self.err.exceptions, e))
65
- else:
66
- self.err = ExceptionGroup(self.msg, (e, self.err))
69
+ if self.err is None:
70
+ self.err = err
71
+ elif self.err.message == err.message:
72
+ self.err = ExceptionGroup(err.message, (*self.err.exceptions, *err.exceptions))
73
+ else:
74
+ self.err = ExceptionGroup(err.message, (*self.err.exceptions, err))
67
75
  return self
68
76
 
69
77
  def propagate_err[T](self, res: "Collector[T] | ErrorAccumulator") -> Optional[T]:
@@ -80,18 +88,26 @@ class ErrorPropagator(Result, Protocol):
80
88
  class Error(ErrorPropagator):
81
89
  """Error-only result container"""
82
90
  err: ExceptionGroup
83
- msg: str = ""
84
91
 
85
92
  @classmethod
86
- def from_e(cls, e: Exception, msg: str = "") -> Self:
87
- return cls(ExceptionGroup(msg, (e,)), msg)
93
+ def from_e(cls, e: Exception, msg: str = "") -> "Error":
94
+ return cls(ExceptionGroup(msg, (e,)))
95
+
96
+ def with_msg(self, msg: str) -> "Error":
97
+ """Returns a new Error instance with updated message context
98
+ while preserving the original exception structure
99
+ """
100
+ return Error(err=ExceptionGroup(msg, (self.err,)))
101
+
102
+ def unwrap(self) -> Never:
103
+ """Always raises exception"""
104
+ raise self.err
88
105
 
89
106
 
90
107
  @dataclass(slots=True)
91
108
  class ErrorAccumulator(ErrorPropagator):
92
109
  """Base container for error propagation with status conversion"""
93
110
  err: Optional[ExceptionGroup] = field(init=False, default=None)
94
- msg: str = ""
95
111
 
96
112
  @property
97
113
  def result(self) -> Ok | Error:
@@ -118,7 +134,6 @@ class Collector[T](ErrorPropagator, Protocol):
118
134
  class Simple[T](Collector[T], Result):
119
135
  """Basic collector for values"""
120
136
  value: T
121
- msg: str = ""
122
137
  err: Optional[ExceptionGroup] = field(init=False, default=None)
123
138
 
124
139
  def set(self, res: "Simple[T]") -> T:
@@ -146,7 +161,6 @@ class Option[T](Simple[Optional[T]], Result):
146
161
  class List[T](Collector[list[Optional[T | Ok]]], Result):
147
162
  """List collector with error accumulation"""
148
163
  value: list[Optional[T] | Ok] = field(init=False, default_factory=list)
149
- msg: str = ""
150
164
  err: Optional[ExceptionGroup] = field(init=False, default=None)
151
165
 
152
166
  def append(self, res: Option[T] | Simple[T] | Error | Ok) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: StructResult
3
- Version: 0.8.2
3
+ Version: 0.8.4
4
4
  Summary: structural result with ExceptionGroup
5
5
  Author-email: Serj Kotilevski <youserj@outlook.com>
6
6
  Project-URL: Source, https://github.com/youserj/Result_prj
@@ -39,4 +39,4 @@ print(f" isinstance(): {time_isinstance_ok:.6f} sec (1M calls)")
39
39
 
40
40
  print(f"\nError-res:")
41
41
  print(f" is_ok(): {time_is_ok_err:.6f} sec (1M calls)")
42
- print(f" isinstance(): {time_isinstance_err:.6f} sec (1M calls)")
42
+ print(f" isinstance(): {time_isinstance_err:.6f} sec (1M calls)")
@@ -1,5 +1,5 @@
1
1
  import unittest
2
- from src.StructResult.result import Option, Error, List
2
+ from src.StructResult.result import Option, Error, List, Simple
3
3
  from src.StructResult.formatter import format_eg
4
4
 
5
5
 
@@ -16,11 +16,11 @@ class TestFormatEG(unittest.TestCase):
16
16
  ])
17
17
 
18
18
  # Create test Result objects
19
- self.simple_result = Option[str](value="test")
19
+ self.simple_result = Option("test")
20
20
  self.error_result = Error.from_e(self.simple_error, msg="error occurred")
21
21
  self.list_result = List[int]()
22
- self.list_result.append(Option[int](value=42))
23
- self.list_result.append(Option[int](value=None))
22
+ self.list_result.append(Simple(42))
23
+ self.list_result.append(Option())
24
24
 
25
25
  def test_basic_exception_group(self) -> None:
26
26
  eg = ExceptionGroup("Test", [self.simple_error])
@@ -46,7 +46,7 @@ class TestFormatEG(unittest.TestCase):
46
46
  self.assertEqual(result, expected)
47
47
 
48
48
  def test_with_result_protocol(self) -> None:
49
- error_result = Option[str]()
49
+ error_result: Option[int] = Option()
50
50
  error_result.append_err(self.complex_group)
51
51
  if error_result.err is not None:
52
52
  result = format_eg(error_result.err)
@@ -74,9 +74,9 @@ class TestFormatEG(unittest.TestCase):
74
74
 
75
75
  def test_with_list_result_errors(self) -> None:
76
76
  list_result = List[int]()
77
- list_result.append(Option[int](value=1))
78
- error_result = Option[int](value=2)
79
- error_result.append_err(self.simple_error)
77
+ list_result.append(Simple(1))
78
+ error_result = Simple(2)
79
+ error_result.append_e(self.simple_error)
80
80
  list_result.append(error_result)
81
81
 
82
82
  if list_result.err:
@@ -85,7 +85,7 @@ class TestFormatEG(unittest.TestCase):
85
85
  self.assertTrue(result.endswith(expected))
86
86
 
87
87
  def test_protocol_compatibility(self) -> None:
88
- error_result = Error(self.complex_group, msg="test")
88
+ error_result = Error(self.complex_group).with_msg(msg="test")
89
89
 
90
90
  self.assertIsInstance(error_result.err, BaseExceptionGroup)
91
91
  if error_result.err is not None:
@@ -14,35 +14,34 @@ class TestResultSystem(unittest.TestCase):
14
14
  err = Error.from_e(exc, "context")
15
15
  self.assertFalse(err.is_ok())
16
16
  self.assertIsNotNone(err.err)
17
- self.assertEqual(err.msg, "context")
18
17
  self.assertEqual(len(err.err.exceptions), 1)
19
18
  self.assertIsInstance(err.err.exceptions[0], ValueError)
20
19
 
21
20
  def test_simple_success(self) -> None:
22
- res = Option[str]("success", "test")
21
+ res: Option[str] = Option("success")
23
22
  self.assertTrue(res.is_ok())
24
23
  self.assertEqual(res.unwrap(), "success")
25
24
  self.assertIsNone(res.err)
26
25
 
27
26
  def test_simple_failure(self) -> None:
28
27
  exc = TypeError("type error")
29
- res = Option[str]("test").append_err(exc)
28
+ res: Option[str] = Option("test").append_e(exc)
30
29
  self.assertFalse(res.is_ok())
31
30
  self.assertIsNotNone(res.err)
32
31
  with self.assertRaises(ExceptionGroup):
33
32
  res.unwrap()
34
33
 
35
34
  def test_bool_type(self) -> None:
36
- true_res = Bool(value=True, msg="test")
37
- false_res = Bool(value=False, msg="test")
35
+ true_res = Bool(value=True)
36
+ false_res = Bool(value=False)
38
37
  self.assertTrue(true_res.unwrap())
39
38
  self.assertFalse(false_res.unwrap())
40
39
 
41
40
  def test_error_propagation(self) -> None:
42
41
  exc1 = RuntimeError("error 1")
43
42
  exc2 = KeyError("error 2")
44
- res1 = Option[int](msg="op1").append_err(exc1)
45
- res2 = Option[int](msg="op2").append_err(exc2)
43
+ res1: Option[int] = Option().append_e(exc1, "op1")
44
+ res2: Option[int] = Option().append_e(exc2, "op2")
46
45
  res1.propagate_err(res2)
47
46
  self.assertFalse(res1.is_ok())
48
47
  if res1.err is not None:
@@ -51,21 +50,21 @@ class TestResultSystem(unittest.TestCase):
51
50
  self.assertIsInstance(res1.err.exceptions[1].exceptions[0], KeyError)
52
51
 
53
52
  def test_list_collector(self) -> None:
54
- lst = List[int]("collection")
55
- lst.append(Option[int](42, "item1"))
53
+ lst: List[int] = List()
54
+ lst.append(Option(42))
56
55
  lst.append(Error.from_e(ValueError("bad value"), "item2"))
57
56
  lst.append(OK)
58
- lst.append(Option[int](100, "item3"))
57
+ lst.append(Option(100))
59
58
  self.assertEqual(len(lst.value), 4)
60
59
  self.assertEqual(lst.value[0], 42)
61
60
  self.assertIsInstance(lst.value[2], Ok)
62
61
  self.assertEqual(lst.value[3], 100)
63
62
  self.assertFalse(lst.is_ok())
64
63
  self.assertEqual(len(lst.err.exceptions), 1)
65
- self.assertIsInstance(lst.err.exceptions[0].exceptions[0], ValueError)
64
+ self.assertIsInstance(lst.err.exceptions[0], ValueError)
66
65
 
67
66
  def test_list_operator_overload(self) -> None:
68
- lst = List[str]("test") + Option[str]("hello", "first")
67
+ lst: List[str] = List[str]() + Option("hello")
69
68
  lst += Error.from_e(TypeError("type error"), "second")
70
69
  self.assertEqual(len(lst.value), 2)
71
70
  self.assertEqual(lst.value[0], "hello")
@@ -75,8 +74,8 @@ class TestResultSystem(unittest.TestCase):
75
74
  exc1 = ValueError("val1")
76
75
  exc2 = TypeError("type1")
77
76
  exc3 = KeyError("key1")
78
- res1: Option[object] = Option(msg="group1").append_err(exc1).append_err(exc2)
79
- res2: Option[object] = Option(msg="group1").append_err(exc3)
77
+ res1: Option[object] = Option().append_e(exc1, "group1").append_e(exc2)
78
+ res2: Option[object] = Option().append_e(exc3, "group1")
80
79
  res1.propagate_err(res2)
81
80
  self.assertEqual(len(res1.err.exceptions), 3)
82
81
  self.assertEqual(res1.err.message, "group1")
@@ -84,35 +83,35 @@ class TestResultSystem(unittest.TestCase):
84
83
  def test_different_error_groups(self) -> None:
85
84
  exc1 = ValueError("val1")
86
85
  exc2 = TypeError("type1")
87
- res1: Option[object] = Option(msg="group1").append_err(exc1)
88
- res2: Option[object] = Option(msg="group2").append_err(exc2)
86
+ res1: Option[object] = Option().append_e(exc1, "group1")
87
+ res2: Option[object] = Option().append_e(exc2, "group2")
89
88
  res1.propagate_err(res2)
90
89
  self.assertEqual(len(res1.err.exceptions), 2)
91
90
  self.assertIsInstance(res1.err.exceptions[1], ExceptionGroup)
92
91
 
93
92
  def test_set_operation(self) -> None:
94
- main: Option[str] = Option(msg="main")
95
- other: Option[str] = Option("data", "other")
93
+ main: Option[str] = Option()
94
+ other: Option[str] = Option("data")
96
95
  result = main.set(other)
97
96
  self.assertEqual(main.value, "data")
98
97
  self.assertEqual(result, "data")
99
98
  self.assertTrue(main.is_ok())
100
99
 
101
100
  def test_bool_set_operation(self) -> None:
102
- main = Bool(msg="main")
103
- other = Bool(value=True, msg="other")
101
+ main = Bool()
102
+ other = Bool(value=True)
104
103
  result = main.set(other)
105
104
  self.assertTrue(main.value)
106
105
  self.assertTrue(result)
107
106
 
108
107
  def test_simple_none_value(self) -> None:
109
- res: Option[int] = Option(msg="test")
108
+ res: Option[int] = Option()
110
109
  self.assertTrue(res.is_ok())
111
110
  self.assertIsNone(res.unwrap())
112
111
 
113
112
  def test_bool_false_with_error(self) -> None:
114
- res = Bool(value=False, msg="test")
115
- res.append_err(ValueError("bool error"))
113
+ res = Bool(value=False)
114
+ res.append_e(ValueError("bool error"))
116
115
  self.assertFalse(res.value)
117
116
  self.assertFalse(res.is_ok())
118
117
  with self.assertRaises(ExceptionGroup):
@@ -121,23 +120,23 @@ class TestResultSystem(unittest.TestCase):
121
120
  def test_nested_exception_groups(self) -> None:
122
121
  inner_group = ExceptionGroup("inner", [ValueError("v1"), TypeError("t1")])
123
122
  outer_group = ExceptionGroup("outer", [inner_group, KeyError("k1")])
124
- res = Option[int](msg="test").append_err(outer_group)
123
+ res: Option[int] = Option().append_err(outer_group)
125
124
  if res.err is not None:
126
- self.assertEqual(len(res.err.exceptions), 1)
125
+ self.assertEqual(len(res.err.exceptions), 2)
127
126
  self.assertIsInstance(res.err.exceptions[0], ExceptionGroup)
128
- self.assertEqual(res.err.exceptions[0].message, "outer")
129
- self.assertEqual(res.err.exceptions[0].exceptions[0].message, "inner")
127
+ self.assertEqual(res.err.exceptions[0].message, "inner")
128
+ self.assertEqual(res.err.message, "outer")
130
129
 
131
130
  def test_list_empty(self) -> None:
132
- lst = List[str]("empty")
131
+ lst: List[str] = List()
133
132
  self.assertTrue(lst.is_ok())
134
133
  self.assertEqual(len(lst.value), 0)
135
134
  self.assertIsNone(lst.err)
136
135
 
137
136
  def test_list_mixed_types(self) -> None:
138
- lst: List[str | int] = List("mixed")
139
- lst.append(Option(42, "int"))
140
- lst.append(Option("hello", "str"))
137
+ lst: List[str | int] = List()
138
+ lst.append(Option(42))
139
+ lst.append(Option("hello"))
141
140
  lst.append(Error.from_e(ValueError("error"), "error"))
142
141
  self.assertEqual(len(lst.value), 3)
143
142
  self.assertEqual(lst.value[0], 42)
@@ -145,25 +144,25 @@ class TestResultSystem(unittest.TestCase):
145
144
  self.assertIsInstance(lst.value[2], type(None))
146
145
 
147
146
  def test_propagate_none(self) -> None:
148
- res = Option[int](42, "main")
149
- res.propagate_err(Option[int](msg="other"))
147
+ res: Option[int] = Option(42)
148
+ res.propagate_err(Option())
150
149
  self.assertTrue(res.is_ok())
151
150
  self.assertEqual(res.unwrap(), 42)
152
151
 
153
152
  def test_type_hints(self) -> None:
154
153
  def processor() -> Option[str]:
155
- return Option[str]("result", "processor")
154
+ return Option("result")
156
155
 
157
156
  result = processor()
158
157
  value: str | None = result.unwrap()
159
158
  self.assertEqual(value, "result")
160
159
 
161
160
  def test_combined_workflow(self) -> None:
162
- main = List[int]("combined workflow")
163
- main += Option[int](10, "op1")
164
- main += Option[int](20, "op2")
161
+ main: List[int] = List()
162
+ main += Option(10)
163
+ main += Option(20)
165
164
  main += Error.from_e(ValueError("invalid value"), "op3")
166
- main += Option[int](30, "op4")
165
+ main += Option(30)
167
166
  self.assertEqual(len(main.value), 4)
168
167
  self.assertEqual(main.value[0], 10)
169
168
  self.assertEqual(main.value[1], 20)
@@ -174,10 +173,10 @@ class TestResultSystem(unittest.TestCase):
174
173
  main.unwrap()
175
174
 
176
175
  def test_multiple_propagations(self) -> None:
177
- res1: Option[int] = Option(msg="first").append_err(ValueError("v1"))
178
- res2: Option[int] = Option(msg="second").append_err(TypeError("t1"))
179
- res3: Option[int] = Option(msg="third").append_err(KeyError("k1"))
180
- main: Option[int] = Option(msg="main")
176
+ res1: Option[int] = Option().append_e(ValueError("v1"), msg="first")
177
+ res2: Option[int] = Option().append_e(TypeError("t1"), msg="second")
178
+ res3: Option[int] = Option().append_e(KeyError("k1"), msg="third")
179
+ main: Option[int] = Option()
181
180
  main.propagate_err(res1)
182
181
  main.propagate_err(res2)
183
182
  main.propagate_err(res3)
@@ -185,29 +184,29 @@ class TestResultSystem(unittest.TestCase):
185
184
 
186
185
  @patch.object(Option, "append_err")
187
186
  def test_propagate_err_calls(self, mock_append: Any) -> None:
188
- err_res = Option[int](msg="error")
187
+ err_res: Option[int] = Option()
189
188
  err_res.err = ExceptionGroup("error", [ValueError("test")])
190
- main = Option[int](msg="main")
189
+ main: Option[int] = Option()
191
190
  main.propagate_err(err_res)
192
191
  mock_append.assert_called_once_with(err_res.err)
193
192
 
194
193
  def test_iterator_protocol(self) -> None:
195
- res = Option[str]("test", "iter")
194
+ res: Option[str] = Option("test")
196
195
  values = list(res)
197
196
  self.assertEqual(len(values), 2)
198
197
  self.assertEqual(values[0], "test")
199
198
  self.assertIsNone(values[1])
200
199
 
201
200
  def test_bool_truthiness(self) -> None:
202
- true_res = Bool(value=True, msg="true")
203
- false_res = Bool(value=False, msg="false")
201
+ true_res = Bool(value=True)
202
+ false_res = Bool(value=False)
204
203
  self.assertTrue(true_res.value)
205
204
  self.assertFalse(false_res.value)
206
205
  self.assertTrue(bool(true_res.unwrap()))
207
206
  self.assertFalse(bool(false_res.unwrap()))
208
207
 
209
208
  def test_equal(self) -> None:
210
- res: Option[str] = Option(msg="1")
209
+ res: Option[str] = Option()
211
210
  group = ExceptionGroup("1", (ValueError("1"),))
212
211
  res.append_err(group)
213
212
  self.assertEqual(res.err, group)
File without changes
File without changes