Skip to content

Instantly share code, notes, and snippets.

@Spitfire1900
Last active January 1, 2025 23:38
Show Gist options
  • Save Spitfire1900/ce2adf8bf2f68176f8b30b5500fdc770 to your computer and use it in GitHub Desktop.
Save Spitfire1900/ce2adf8bf2f68176f8b30b5500fdc770 to your computer and use it in GitHub Desktop.
Rust, Go, and custom Union style Error as Values for Python
import enum
import typing
from enum import Enum
from typing import Any, Callable, Generic, Literal, ParamSpec, Tuple, TypeVar, Union
P = ParamSpec("P")
R = TypeVar("R")
T = TypeVar("T")
__all__: list[str] = [
"box",
"split",
"either",
"WrappedException",
"Result",
]
class WrappedException(Exception):
'''
Exception class that wraps another exception for clearer error handling.
This class allows exceptions to be caught and handled in a structured way,
providing a clear indication of the original error.
Args:
exception (Exception): The exception to wrap.
'''
__match_args__: Tuple[Literal['__cause__']] = ("__cause__", )
def __init__(self, exception: Exception) -> None:
self.__context__ = exception
self.__cause__ = exception
class ResultType(Enum):
OK = enum.auto()
ERR = enum.auto()
class Result(Generic[T]):
'''
Represents the outcome of an operation, encapsulating either a successful result or an error.
Inspired by the Result type in Rust, this class provides a way to handle success and error states
in a type-safe manner. It allows functions to return values without relying on exceptions for control flow.
The Result class has two possible states: OK, which contains a valid result, and ERR, which contains
an error wrapped in a WrappedException. This design encourages explicit error handling in client code.
Attributes:
result_type (ResultType): The type indicating whether the result is OK or an error.
val (T): The value of the result, which can be a successful value or a WrappedException.
Args:
result_type (ResultType): The result type indicating success (OK) or failure (ERR).
val (T): The value to encapsulate in the Result, which may be a valid result or an error.
Methods:
__repr__(): Returns a string representation of the Result.
is_ok() -> bool: Returns True if the result is OK, otherwise False.
is_err() -> bool: Returns True if the result is an error, otherwise False.
unwrap() -> T: Returns the encapsulated value if it is OK; raises a WrappedException if it is an error.
'''
def __init__(self, result_type: ResultType,
val: T | WrappedException) -> None:
self.result_type: ResultType = result_type
self.val: T | WrappedException
match result_type:
case ResultType.OK:
match val:
case Exception():
raise ValueError(
f"Cannot create an OK Result with an exception: {val}"
)
case _:
self.val = val
case ResultType.ERR:
match val:
case WrappedException():
self.val = val
case Exception():
self.val = WrappedException(val)
case _:
raise ValueError(
f"Cannot create an ERR Result with a non-exception: {val}"
)
def __repr__(self) -> str:
return f"Result({repr(self.val)})"
def is_ok(self) -> bool:
return not isinstance(self.val, Exception)
def is_err(self) -> bool:
return isinstance(self.val, Exception)
def unwrap(self) -> T:
'''
Returns the value of the result if it's successful.
Raises:
WrappedException: If the result is an error, raises the wrapped exception.
'''
match self.val:
case WrappedException() as err:
raise err
case _:
return self.val
def box(fn: Callable[P, R], *my_args: P.args,
**my_kwargs: P.kwargs) -> Result[WrappedException] | Result[R]:
'''
Executes a function and returns a Result indicating success or failure.
Inspired by the Rust programming language's error handling style,
this function will return a Result that either contains a successful
result or a WrappedException if an error occurs.
If the function executes successfully, returns a Result containing the result.
If an exception occurs, returns a Result containing a WrappedException.
Args:
fn (Callable[P, R]): The function to execute.
*my_args: Positional arguments to pass to the function.
**my_kwargs: Keyword arguments to pass to the function.
Returns:
Result[WrappedException] | Result[R]: The result of the function execution.
'''
try:
val = fn(*my_args, **my_kwargs)
return Result(result_type=ResultType.OK, val=val)
except Exception as _err: # pylint: disable=broad-exception-caught
return Result(result_type=ResultType.ERR, val=WrappedException(_err))
def split(fn: Callable[P, R], *my_args: P.args,
**my_kwargs: P.kwargs) -> Tuple[R, None] | Tuple[None, Exception]:
'''
Executes a function and returns a tuple indicating success or failure.
Inspired by Go's error handling idiom, this function will return a tuple
where the first element is the successful result (or None in case of error)
and the second element is an Exception (or None if successful).
If the function executes successfully, returns a tuple containing the result and None.
If an exception occurs, returns a tuple containing None and a WrappedException.
Args:
fn (Callable[P, R]): The function to execute.
*my_args: Positional arguments to pass to the function.
**my_kwargs: Keyword arguments to pass to the function.
Returns:
Tuple[R, None] | Tuple[None, Exception]: The result of the function execution.
'''
try:
return (fn(*my_args, **my_kwargs), None)
except Exception as _err: # pylint: disable=broad-exception-caught
return (None, WrappedException(_err))
def either(fn: Callable[P, R], *my_args: P.args,
**my_kwargs: P.kwargs) -> Union[WrappedException, R]:
'''
Executes a function and returns either the result or a WrappedException.
Inspired by functional programming practices, this function provides a
straightforward way to handle errors by returning either the successful result
or a WrappedException if an error occurs.
If the function executes successfully, returns the result.
If an exception occurs, returns a WrappedException.
Args:
fn (Callable[P, R]): The function to execute.
*my_args: Positional arguments to pass to the function.
**my_kwargs: Keyword arguments to pass to the function.
Returns:
Union[WrappedException, R]: The result of the function execution or a WrappedException.
'''
try:
#
return fn(*my_args, **my_kwargs)
except Exception as _err: # pylint: disable=broad-ex
return WrappedException(_err)
def add(x: typing.Any, y: typing.Any = 4) -> typing.Any:
return x + y
def divide(x: typing.Any, y: typing.Any = 4) -> typing.Any:
return x / y
_box: Result[WrappedException] | Result[Any] = box(divide, 4, y=4)
match _box.val:
case WrappedException(TypeError()) as e:
raise RuntimeError("You need to pass a int for each value") from e
case WrappedException() as e:
raise e from e
case val:
print(f"everything was fine, got {val}")
v = val
r = Result(result_type=ResultType.OK, val=5)
if r.is_ok():
v: int = r.unwrap() + 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment