Coverage for src / taipanstack / core / result.py: 100%
91 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-12 21:18 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-12 21:18 +0000
1"""
2Result type utilities for functional error handling.
4Provides Rust-style Result types (Ok/Err) for explicit error handling,
5avoiding exceptions for expected failure cases. This promotes safer,
6more predictable code.
8Example:
9 >>> from taipanstack.core.result import safe, Ok, Err
10 >>> @safe
11 ... def divide(a: int, b: int) -> float:
12 ... if b == 0:
13 ... raise ValueError("division by zero")
14 ... return a / b
15 >>> result = divide(10, 0)
16 >>> match result:
17 ... case Err(e):
18 ... print(f"Error: {e}")
19 ... case Ok(value):
20 ... print(f"Result: {value}")
21 Error: division by zero
23"""
25import functools
26import inspect
27from collections.abc import Awaitable, Callable, Iterable
28from typing import ParamSpec, Protocol, TypeVar, cast, overload
30from result import Err, Ok, Result
32__all__ = [
33 "Err",
34 "Ok",
35 "Result",
36 "and_then_async",
37 "collect_results",
38 "map_async",
39 "safe",
40 "safe_from",
41]
43P = ParamSpec("P")
44T = TypeVar("T")
45E = TypeVar("E", bound=Exception)
46E_co = TypeVar("E_co", bound=Exception, covariant=True)
47U = TypeVar("U")
50@overload
51def safe(
52 func: Callable[P, T],
53) -> Callable[P, Result[T, Exception]]: ...
56@overload
57def safe(
58 func: Callable[P, Awaitable[T]],
59) -> Callable[P, Awaitable[Result[T, Exception]]]: ...
62def safe(
63 func: Callable[P, T] | Callable[P, Awaitable[T]],
64) -> Callable[P, Result[T, Exception]] | Callable[P, Awaitable[Result[T, Exception]]]:
65 """Wrap a sync or async function to convert exceptions into Err results.
67 Detect whether *func* is a coroutine function and choose the
68 appropriate wrapper so that ``await``-able functions remain
69 ``await``-able and synchronous functions stay synchronous.
71 Args:
72 func: The sync or async function to wrap.
74 Returns:
75 A wrapped function that returns ``Result[T, Exception]``
76 (or a coroutine resolving to one).
78 Example:
79 >>> @safe
80 ... def parse_int(s: str) -> int:
81 ... return int(s)
82 >>> parse_int("42")
83 Ok(42)
84 >>> parse_int("invalid")
85 Err(ValueError("invalid literal for int()..."))
87 """
88 # Pre-cache constructors for minor speedup in tight loops
89 # (LOAD_DEREF is faster than LOAD_GLOBAL)
90 ok_cls = Ok
91 err_cls = Err
93 if inspect.iscoroutinefunction(func):
94 # Cast once here to satisfy mypy inside the closure
95 func_coro = cast(Callable[P, Awaitable[T]], func)
97 @functools.wraps(func)
98 async def async_wrapper(
99 *args: P.args,
100 **kwargs: P.kwargs,
101 ) -> Result[T, Exception]:
102 try:
103 return ok_cls(await func_coro(*args, **kwargs))
104 except Exception as e:
105 return err_cls(e)
107 return cast(Callable[P, Awaitable[Result[T, Exception]]], async_wrapper)
109 # Cast once here to satisfy mypy inside the closure
110 func_sync = cast(Callable[P, T], func)
112 @functools.wraps(func)
113 def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[T, Exception]:
114 try:
115 return ok_cls(func_sync(*args, **kwargs))
116 except Exception as e:
117 return err_cls(e)
119 return cast(Callable[P, Result[T, Exception]], wrapper)
122class SafeFromDecorator(Protocol[E_co]):
123 """Protocol for safe_from decorator."""
125 @overload
126 def __call__(self, func: Callable[P, T]) -> Callable[P, Result[T, E_co]]: ...
128 @overload
129 def __call__(
130 self, func: Callable[P, Awaitable[T]]
131 ) -> Callable[P, Awaitable[Result[T, E_co]]]: ...
134def safe_from(
135 *exception_types: type[E],
136) -> SafeFromDecorator[E]:
137 """Decorator factory to catch specific exceptions as Err.
139 Only catches specified exception types; others propagate normally.
141 Args:
142 *exception_types: Exception types to convert to Err.
144 Returns:
145 Decorator that wraps function with selective error handling.
147 Example:
148 >>> @safe_from(ValueError, TypeError)
149 ... def process(data: str) -> int:
150 ... return int(data)
151 >>> process("abc")
152 Err(ValueError(...))
154 """
156 def decorator(
157 func: Callable[P, T] | Callable[P, Awaitable[T]],
158 ) -> Callable[P, Result[T, E]] | Callable[P, Awaitable[Result[T, E]]]:
159 if inspect.iscoroutinefunction(func):
160 func_coro = cast(Callable[P, Awaitable[T]], func)
162 @functools.wraps(func)
163 async def async_wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[T, E]:
164 try:
165 return Ok(await func_coro(*args, **kwargs))
166 except exception_types as e:
167 return Err(e)
169 return cast(Callable[P, Awaitable[Result[T, E]]], async_wrapper)
171 func_sync = cast(Callable[P, T], func)
173 @functools.wraps(func)
174 def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[T, E]:
175 try:
176 return Ok(func_sync(*args, **kwargs))
177 except exception_types as e:
178 return Err(e)
180 return cast(Callable[P, Result[T, E]], wrapper)
182 return cast(SafeFromDecorator[E], decorator)
185def _collect_list(
186 results: list[Result[T, E]] | tuple[Result[T, E], ...],
187) -> Result[list[T], E] | None:
188 try:
189 # We use a runtime # type: ignore to bypass mypy's strict check
190 # on the AttributeError strategy for extreme performance on the hot path
191 return Ok([r.ok_value for r in results]) # type: ignore[union-attr]
192 except AttributeError:
193 return None
196def collect_results(
197 results: Iterable[Result[T, E]],
198) -> Result[list[T], E]:
199 """Collect an iterable of Results into a single Result.
201 If all results are Ok, returns Ok with list of values.
202 If any result is Err, returns the first Err encountered.
204 Args:
205 results: Iterable of Result objects.
207 Returns:
208 Ok(list[T]) if all are Ok, otherwise first Err.
210 Example:
211 >>> collect_results([Ok(1), Ok(2), Ok(3)])
212 Ok([1, 2, 3])
213 >>> collect_results([Ok(1), Err("fail"), Ok(3)])
214 Err("fail")
216 """
217 if isinstance(results, (list, tuple)):
218 optimized_res = _collect_list(results)
219 if optimized_res is not None:
220 return optimized_res
222 ok_cls = Ok
223 err_cls = Err
224 values: list[T] = []
225 append = values.append
226 for result in results:
227 # We use explicit type checks (isinstance) but pre-cache the constructors.
228 # type(result) is ok_cls would be even faster but breaks subclassing.
229 if isinstance(result, ok_cls):
230 append(result.ok_value)
231 elif isinstance(result, err_cls):
232 return result
233 else:
234 # Fallback for structural compatibility
235 return result
236 return ok_cls(values)
239@overload
240async def map_async(
241 result: Ok[T],
242 func: Callable[[T], Awaitable[U]],
243) -> Result[U, E]: ...
246@overload
247async def map_async(
248 result: Err[E],
249 func: Callable[[T], Awaitable[U]],
250) -> Err[E]: ...
253@overload
254async def map_async(
255 result: Result[T, E],
256 func: Callable[[T], Awaitable[U]],
257) -> Result[U, E]: ...
260async def map_async(
261 result: Result[T, E],
262 func: Callable[[T], Awaitable[U]],
263) -> Result[U, E]:
264 """Asynchronously apply a function to the value of an Ok result.
266 If the result is Err, returns it unchanged.
268 Args:
269 result: The Result to process.
270 func: Awaitable function to apply to the Ok value.
272 Returns:
273 New Result containing the processed value or original error.
275 Example:
276 >>> async def process(x: int) -> str:
277 ... return str(x * 2)
278 >>> await map_async(Ok(5), process)
279 Ok('10')
280 >>> await map_async(Err("fail"), process)
281 Err('fail')
283 """
284 match result:
285 case Ok(val):
286 return Ok(await func(val))
287 case Err(e):
288 _ = e
289 return result
290 case _:
291 return result
294@overload
295async def and_then_async(
296 result: Ok[T],
297 func: Callable[[T], Awaitable[Result[U, E]]],
298) -> Result[U, E]: ...
301@overload
302async def and_then_async(
303 result: Err[E],
304 func: Callable[[T], Awaitable[Result[U, E]]],
305) -> Err[E]: ...
308@overload
309async def and_then_async(
310 result: Result[T, E],
311 func: Callable[[T], Awaitable[Result[U, E]]],
312) -> Result[U, E]: ...
315async def and_then_async(
316 result: Result[T, E],
317 func: Callable[[T], Awaitable[Result[U, E]]],
318) -> Result[U, E]:
319 """Asynchronously chain operations that return Results.
321 If the result is Ok, asynchronously applies `func` and returns its Result.
322 If the result is Err, returns it unchanged.
324 Args:
325 result: The Result to process.
326 func: Awaitable function taking the Ok value and returning a new Result.
328 Returns:
329 The new Result from `func` or the original error.
331 Example:
332 >>> async def fetch_user(uid: int) -> Result[str, ValueError]:
333 ... if uid == 1:
334 ... return Ok("Alice")
335 ... return Err(ValueError("User not found"))
336 >>> await and_then_async(Ok(1), fetch_user)
337 Ok('Alice')
338 >>> await and_then_async(Ok(2), fetch_user)
339 Err(ValueError('User not found'))
340 >>> await and_then_async(Err(ValueError("No DB")), fetch_user)
341 Err(ValueError('No DB'))
343 """
344 match result:
345 case Ok(val):
346 return await func(val)
347 case Err(e):
348 _ = e
349 return result
350 case _:
351 return result