diff --git a/pyropust/__init__.pyi b/pyropust/__init__.pyi index ab4c7a1..ad76139 100644 --- a/pyropust/__init__.pyi +++ b/pyropust/__init__.pyi @@ -11,11 +11,28 @@ Out_co = TypeVar("Out_co", covariant=True) class Result(Generic[T_co, E_co]): def is_ok(self) -> bool: ... def is_err(self) -> bool: ... + def is_ok_and(self, predicate: Callable[[T_co], object]) -> bool: ... + def is_err_and(self, predicate: Callable[[E_co], object]) -> bool: ... def unwrap(self) -> T_co: ... def unwrap_err(self) -> E_co: ... + def expect(self, msg: str) -> T_co: ... + def expect_err(self, msg: str) -> E_co: ... + def unwrap_or[U](self, default: U) -> T_co | U: ... + def unwrap_or_else[U](self, f: Callable[[E_co], U]) -> T_co | U: ... + def ok(self) -> Option[T_co]: ... + def err(self) -> Option[E_co]: ... def map[U](self, f: Callable[[T_co], U]) -> Result[U, E_co]: ... def map_err[U](self, f: Callable[[E_co], U]) -> Result[T_co, U]: ... + def map_or[U](self, default: U, f: Callable[[T_co], U]) -> U: ... + def map_or_else[U](self, default_f: Callable[[E_co], U], f: Callable[[T_co], U]) -> U: ... + def inspect(self, f: Callable[[T_co], object]) -> Result[T_co, E_co]: ... + def inspect_err(self, f: Callable[[E_co], object]) -> Result[T_co, E_co]: ... + def and_[U](self, other: Result[U, E_co]) -> Result[U, E_co]: ... + def or_[F](self, other: Result[T_co, F]) -> Result[T_co, F]: ... + def or_else[F](self, f: Callable[[E_co], Result[T_co, F]]) -> Result[T_co, F]: ... def and_then[U](self, f: Callable[[T_co], Result[U, E_co]]) -> Result[U, E_co]: ... + def flatten[T, E](self: Result[Result[T, E], E]) -> Result[T, E]: ... + def transpose[T, E](self: Result[Option[T], E]) -> Option[Result[T, E]]: ... def unwrap_or_raise(self, exc: BaseException) -> T_co: ... @classmethod def attempt[T]( diff --git a/pyropust/pyropust_native.pyi b/pyropust/pyropust_native.pyi index 7846fa1..b8b041c 100644 --- a/pyropust/pyropust_native.pyi +++ b/pyropust/pyropust_native.pyi @@ -17,11 +17,28 @@ Out_co = TypeVar("Out_co", covariant=True) class Result(Generic[T_co, E_co]): def is_ok(self) -> bool: ... def is_err(self) -> bool: ... + def is_ok_and(self, predicate: Callable[[T_co], object]) -> bool: ... + def is_err_and(self, predicate: Callable[[E_co], object]) -> bool: ... def unwrap(self) -> T_co: ... def unwrap_err(self) -> E_co: ... + def expect(self, msg: str) -> T_co: ... + def expect_err(self, msg: str) -> E_co: ... + def unwrap_or[U](self, default: U) -> T_co | U: ... + def unwrap_or_else[U](self, f: Callable[[E_co], U]) -> T_co | U: ... + def ok(self) -> Option[T_co]: ... + def err(self) -> Option[E_co]: ... def map[U](self, f: Callable[[T_co], U]) -> Result[U, E_co]: ... def map_err[U](self, f: Callable[[E_co], U]) -> Result[T_co, U]: ... + def map_or[U](self, default: U, f: Callable[[T_co], U]) -> U: ... + def map_or_else[U](self, default_f: Callable[[E_co], U], f: Callable[[T_co], U]) -> U: ... + def inspect(self, f: Callable[[T_co], object]) -> Result[T_co, E_co]: ... + def inspect_err(self, f: Callable[[E_co], object]) -> Result[T_co, E_co]: ... + def and_[U](self, other: Result[U, E_co]) -> Result[U, E_co]: ... + def or_[F](self, other: Result[T_co, F]) -> Result[T_co, F]: ... + def or_else[F](self, f: Callable[[E_co], Result[T_co, F]]) -> Result[T_co, F]: ... def and_then[U](self, f: Callable[[T_co], Result[U, E_co]]) -> Result[U, E_co]: ... + def flatten[T, E](self: Result[Result[T, E], E]) -> Result[T, E]: ... + def transpose[T, E](self: Result[Option[T], E]) -> Option[Result[T, E]]: ... def unwrap_or_raise(self, exc: BaseException) -> T_co: ... @classmethod def attempt[T]( diff --git a/src/py/result.rs b/src/py/result.rs index 973f795..350de42 100644 --- a/src/py/result.rs +++ b/src/py/result.rs @@ -4,6 +4,7 @@ use pyo3::types::{PyAny, PyTuple, PyType}; use pyo3::Bound; use super::error::build_ropust_error_from_pyerr; +use super::option::{none_, some, OptionObj}; #[pyclass(name = "Result")] pub struct ResultObj { @@ -38,6 +39,56 @@ impl ResultObj { } } + fn expect(&self, py: Python<'_>, msg: &str) -> PyResult> { + if self.is_ok { + Ok(self.ok.as_ref().expect("ok value").clone_ref(py)) + } else { + Err(PyRuntimeError::new_err(msg.to_string())) + } + } + + fn expect_err(&self, py: Python<'_>, msg: &str) -> PyResult> { + if self.is_ok { + Err(PyRuntimeError::new_err(msg.to_string())) + } else { + Ok(self.err.as_ref().expect("err value").clone_ref(py)) + } + } + + fn unwrap_or(&self, py: Python<'_>, default: Py) -> Py { + if self.is_ok { + self.ok.as_ref().expect("ok value").clone_ref(py) + } else { + default + } + } + + fn unwrap_or_else(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult> { + if self.is_ok { + Ok(self.ok.as_ref().expect("ok value").clone_ref(py)) + } else { + let err_value = self.err.as_ref().expect("err value"); + let result = f.call1((err_value.clone_ref(py),))?; + Ok(result.into()) + } + } + + fn ok(&self, py: Python<'_>) -> OptionObj { + if self.is_ok { + some(self.ok.as_ref().expect("ok value").clone_ref(py)) + } else { + none_() + } + } + + fn err(&self, py: Python<'_>) -> OptionObj { + if self.is_ok { + none_() + } else { + some(self.err.as_ref().expect("err value").clone_ref(py)) + } + } + fn map(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult { if self.is_ok { let value = self.ok.as_ref().expect("ok value"); @@ -58,6 +109,113 @@ impl ResultObj { } } + fn map_or( + &self, + py: Python<'_>, + default: Py, + f: Bound<'_, PyAny>, + ) -> PyResult> { + if self.is_ok { + let value = self.ok.as_ref().expect("ok value"); + let result = f.call1((value.clone_ref(py),))?; + Ok(result.into()) + } else { + Ok(default) + } + } + + fn map_or_else( + &self, + py: Python<'_>, + default_f: Bound<'_, PyAny>, + f: Bound<'_, PyAny>, + ) -> PyResult> { + if self.is_ok { + let value = self.ok.as_ref().expect("ok value"); + let result = f.call1((value.clone_ref(py),))?; + Ok(result.into()) + } else { + let err_value = self.err.as_ref().expect("err value"); + let result = default_f.call1((err_value.clone_ref(py),))?; + Ok(result.into()) + } + } + + fn inspect(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult { + if self.is_ok { + let value = self.ok.as_ref().expect("ok value"); + f.call1((value.clone_ref(py),))?; + } + Ok(ResultObj { + is_ok: self.is_ok, + ok: self.ok.as_ref().map(|v| v.clone_ref(py)), + err: self.err.as_ref().map(|v| v.clone_ref(py)), + }) + } + + fn inspect_err(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult { + if !self.is_ok { + let value = self.err.as_ref().expect("err value"); + f.call1((value.clone_ref(py),))?; + } + Ok(ResultObj { + is_ok: self.is_ok, + ok: self.ok.as_ref().map(|v| v.clone_ref(py)), + err: self.err.as_ref().map(|v| v.clone_ref(py)), + }) + } + + fn and_(&self, py: Python<'_>, other: &Self) -> Self { + if self.is_ok { + ResultObj { + is_ok: other.is_ok, + ok: other.ok.as_ref().map(|v| v.clone_ref(py)), + err: other.err.as_ref().map(|v| v.clone_ref(py)), + } + } else { + ResultObj { + is_ok: self.is_ok, + ok: self.ok.as_ref().map(|v| v.clone_ref(py)), + err: self.err.as_ref().map(|v| v.clone_ref(py)), + } + } + } + + fn or_(&self, py: Python<'_>, other: &Self) -> Self { + if self.is_ok { + ResultObj { + is_ok: self.is_ok, + ok: self.ok.as_ref().map(|v| v.clone_ref(py)), + err: self.err.as_ref().map(|v| v.clone_ref(py)), + } + } else { + ResultObj { + is_ok: other.is_ok, + ok: other.ok.as_ref().map(|v| v.clone_ref(py)), + err: other.err.as_ref().map(|v| v.clone_ref(py)), + } + } + } + + fn or_else(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult { + if self.is_ok { + Ok(ResultObj { + is_ok: self.is_ok, + ok: self.ok.as_ref().map(|v| v.clone_ref(py)), + err: self.err.as_ref().map(|v| v.clone_ref(py)), + }) + } else { + let err_value = self.err.as_ref().expect("err value"); + let out = f.call1((err_value.clone_ref(py),))?; + let result_type = py.get_type::(); + if !out.is_instance(result_type.as_any())? { + return Err(PyTypeError::new_err("or_else callback must return Result")); + } + let out_ref: PyRef<'_, ResultObj> = out.extract()?; + Ok(clone_result(py, &out_ref)) + } + } + fn and_then(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult { if self.is_ok { let value = self.ok.as_ref().expect("ok value"); @@ -73,6 +231,68 @@ impl ResultObj { } } + fn is_ok_and(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult { + if self.is_ok { + let value = self.ok.as_ref().expect("ok value"); + let result = f.call1((value.clone_ref(py),))?; + result.is_truthy() + } else { + Ok(false) + } + } + + fn is_err_and(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult { + if self.is_ok { + Ok(false) + } else { + let value = self.err.as_ref().expect("err value"); + let result = f.call1((value.clone_ref(py),))?; + result.is_truthy() + } + } + + fn flatten(&self, py: Python<'_>) -> PyResult { + if self.is_ok { + let value = self.ok.as_ref().expect("ok value"); + let result_type = py.get_type::(); + if !value.bind(py).is_instance(result_type.as_any())? { + return Err(PyTypeError::new_err( + "flatten requires Ok value to be a Result", + )); + } + let inner_ref: PyRef<'_, ResultObj> = value.extract(py)?; + Ok(clone_result(py, &inner_ref)) + } else { + Ok(err(self.err.as_ref().expect("err value").clone_ref(py))) + } + } + + fn transpose(&self, py: Python<'_>) -> PyResult { + if self.is_ok { + let value = self.ok.as_ref().expect("ok value"); + let option_type = py.get_type::(); + if !value.bind(py).is_instance(option_type.as_any())? { + return Err(PyTypeError::new_err( + "transpose requires Ok value to be an Option", + )); + } + let opt_ref: PyRef<'_, OptionObj> = value.extract(py)?; + if opt_ref.is_some { + let inner_value = opt_ref.value.as_ref().expect("some value").clone_ref(py); + let result_obj = ok(inner_value); + let py_result = Py::new(py, result_obj)?; + Ok(some(py_result.into())) + } else { + Ok(none_()) + } + } else { + let err_value = self.err.as_ref().expect("err value").clone_ref(py); + let result_obj = err(err_value); + let py_result = Py::new(py, result_obj)?; + Ok(some(py_result.into())) + } + } + #[classmethod] #[pyo3(signature = (f, *exceptions))] fn attempt( diff --git a/tests/option/__init__.py b/tests/option/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/option/test_methods.py b/tests/option/test_methods.py new file mode 100644 index 0000000..23d7039 --- /dev/null +++ b/tests/option/test_methods.py @@ -0,0 +1,46 @@ +"""Tests for Option methods (map, unwrap_or).""" + +from __future__ import annotations + +from pyropust import None_, Option, Some + + +class TestOptionMap: + """Test Option.map() for transforming Some values.""" + + def test_map_transforms_some_value(self) -> None: + opt = Some(10).map(lambda x: x * 2) + assert opt.is_some() + assert opt.unwrap() == 20 + + def test_map_skips_on_none(self) -> None: + opt: Option[int] = None_().map(lambda x: x * 2) + assert opt.is_none() + + +class TestOptionUnwrapOr: + """Test Option.unwrap_or() for providing default values.""" + + def test_unwrap_or_returns_value_on_some(self) -> None: + opt = Some("Alice") + assert opt.unwrap_or("Guest") == "Alice" + + def test_unwrap_or_returns_default_on_none(self) -> None: + opt: Option[str] = None_() + assert opt.unwrap_or("Guest") == "Guest" + + def test_readme_example_option_usage(self) -> None: + """Verify the README Option example works.""" + + def find_user(user_id: int) -> Option[str]: + return Some("Alice") if user_id == 1 else None_() + + # Found user + name_opt = find_user(1) + name = name_opt.unwrap_or("Guest") + assert name == "Alice" + + # Not found user + name_opt2 = find_user(999) + name2 = name_opt2.unwrap_or("Guest") + assert name2 == "Guest" diff --git a/tests/result/__init__.py b/tests/result/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/result/test_composition.py b/tests/result/test_composition.py new file mode 100644 index 0000000..fc42d48 --- /dev/null +++ b/tests/result/test_composition.py @@ -0,0 +1,246 @@ +"""Tests for Result composition methods (and_then, and_, or_). + +Note: Type annotations are required when using Ok()/Err() constructors +because they have inferred types Result[T, Never] and Result[Never, E]. +This matches Rust's type system design. Use function return types or +intermediate functions to satisfy strict type checking. +""" + +from __future__ import annotations + +from pyropust import Err, Ok, Result + + +class TestResultAndThen: + """Test Result.and_then() for chaining operations that return Result.""" + + def test_and_then_chains_ok_results(self) -> None: + res = Ok(10).and_then(lambda x: Ok(x * 2)) + assert res.is_ok() + assert res.unwrap() == 20 + + def test_and_then_short_circuits_on_err(self) -> None: + res: Result[int, str] = Err("first error").and_then(lambda x: Ok(x * 2)) + assert res.is_err() + assert res.unwrap_err() == "first error" + + def test_and_then_propagates_inner_err(self) -> None: + def get_ok() -> Result[int, str]: + return Ok(10) + + def inner_err(_val: int) -> Result[int, str]: + return Err("inner error") + + res = get_ok().and_then(inner_err) + assert res.is_err() + assert res.unwrap_err() == "inner error" + + def test_readme_example_functional_chaining(self) -> None: + """Verify the README functional chaining example works.""" + res = Ok("123").map(int).map(lambda x: x * 2).and_then(lambda x: Ok(f"Value is {x}")) + assert res.unwrap() == "Value is 246" + + +class TestResultAnd: + """Test Result.and_() for combining Results.""" + + def test_and_returns_other_on_ok(self) -> None: + res1: Result[int, str] = Ok(10) + res2: Result[int, str] = Ok(20) + result = res1.and_(res2) + assert result.is_ok() + assert result.unwrap() == 20 + + def test_and_returns_self_on_err(self) -> None: + res1: Result[int, str] = Err("error1") + res2: Result[int, str] = Ok(20) + result = res1.and_(res2) + assert result.is_err() + assert result.unwrap_err() == "error1" + + def test_and_ok_with_err_returns_err(self) -> None: + # Type annotations are required because Ok() and Err() constructors + # have inferred types Result[T, Never] and Result[Never, E] respectively. + # This matches Rust's type system design. + # Note: Using intermediate function calls to satisfy pyright's strict type checking + def ok_val() -> Result[int, str]: + return Ok(10) + + def err_val() -> Result[int, str]: + return Err("error2") + + res1 = ok_val() + res2 = err_val() + result = res1.and_(res2) + assert result.is_err() + assert result.unwrap_err() == "error2" + + def test_and_both_err_returns_first_err(self) -> None: + res1: Result[int, str] = Err("error1") + res2: Result[int, str] = Err("error2") + result = res1.and_(res2) + assert result.is_err() + assert result.unwrap_err() == "error1" + + def test_and_enables_sequential_validation(self) -> None: + """Use case: sequential validation where all must succeed.""" + + def validate_positive(x: int) -> Result[int, str]: + return Ok(x) if x > 0 else Err("must be positive") + + def validate_range(x: int) -> Result[int, str]: + return Ok(x) if x < 100 else Err("must be less than 100") + + # Success case + result = validate_positive(50).and_(validate_range(50)) + assert result.is_ok() + assert result.unwrap() == 50 + + # First validation fails + result = validate_positive(-5).and_(validate_range(50)) + assert result.is_err() + assert result.unwrap_err() == "must be positive" + + +class TestResultOr: + """Test Result.or_() for fallback Results.""" + + def test_or_returns_self_on_ok(self) -> None: + res1: Result[int, str] = Ok(10) + res2: Result[int, str] = Ok(20) + result = res1.or_(res2) + assert result.is_ok() + assert result.unwrap() == 10 + + def test_or_returns_other_on_err(self) -> None: + # Type annotations are required because Ok() and Err() constructors + # have inferred types Result[T, Never] and Result[Never, E] respectively. + # This matches Rust's type system design. + # Note: Using intermediate function calls to satisfy pyright's strict type checking + def err_val() -> Result[int, str]: + return Err("error1") + + def ok_val() -> Result[int, str]: + return Ok(20) + + res1 = err_val() + res2 = ok_val() + result = res1.or_(res2) + assert result.is_ok() + assert result.unwrap() == 20 + + def test_or_both_ok_returns_first_ok(self) -> None: + res1: Result[int, str] = Ok(10) + res2: Result[int, str] = Ok(20) + result = res1.or_(res2) + assert result.is_ok() + assert result.unwrap() == 10 + + def test_or_both_err_returns_second_err(self) -> None: + res1: Result[int, str] = Err("error1") + res2: Result[int, str] = Err("error2") + result = res1.or_(res2) + assert result.is_err() + assert result.unwrap_err() == "error2" + + def test_or_enables_fallback_chain(self) -> None: + """Use case: try multiple sources until one succeeds.""" + + def fetch_from_cache() -> Result[str, str]: + return Err("cache miss") + + def fetch_from_database() -> Result[str, str]: + return Err("db connection failed") + + def fetch_from_default() -> Result[str, str]: + return Ok("default value") + + # All fallbacks tried until one succeeds + result = fetch_from_cache().or_(fetch_from_database()).or_(fetch_from_default()) + assert result.is_ok() + assert result.unwrap() == "default value" + + def test_or_with_err_ok_returns_ok(self) -> None: + # Type annotations are required because Ok() and Err() constructors + # have inferred types Result[T, Never] and Result[Never, E] respectively. + # This matches Rust's type system design. + # Note: Using intermediate function calls to satisfy pyright's strict type checking + def err_val() -> Result[int, str]: + return Err("error") + + def ok_val() -> Result[int, str]: + return Ok(42) + + res1 = err_val() + res2 = ok_val() + result = res1.or_(res2) + assert result.is_ok() + assert result.unwrap() == 42 + + +class TestResultOrElse: + """Test Result.or_else() for fallback with error transformation.""" + + def test_or_else_returns_self_on_ok(self) -> None: + res: Result[int, str] = Ok(10) + result = res.or_else(lambda _e: Ok(20)) + assert result.is_ok() + assert result.unwrap() == 10 + + def test_or_else_calls_function_on_err(self) -> None: + res: Result[int, str] = Err("error") + # Pyright reports type error due to covariance: Ok(20) returns Result[int, Never], + # but or_else expects Result[T_co, F]. This is a type system limitation when using + # lambdas with inferred types. The runtime behavior is correct and matches Rust. + result = res.or_else(lambda _e: Ok(20)) # pyright: ignore[reportArgumentType] + assert result.is_ok() + assert result.unwrap() == 20 + + def test_or_else_receives_error_value(self) -> None: + """Verify or_else function receives the actual error.""" + res: Result[int, int] = Err(404) + # Pyright reports type error due to covariance: Ok(code * 10) returns Result[int, Never], + # but or_else expects Result[T_co, F]. This is a type system limitation when using + # lambdas with inferred types. The runtime behavior is correct and matches Rust. + result = res.or_else(lambda code: Ok(code * 10)) # pyright: ignore[reportArgumentType] + assert result.is_ok() + assert result.unwrap() == 4040 + + def test_or_else_can_return_new_error(self) -> None: + """Verify or_else can transform error type.""" + res: Result[int, str] = Err("original") + result = res.or_else(lambda e: Err(f"transformed: {e}")) + assert result.is_err() + assert result.unwrap_err() == "transformed: original" + + def test_or_else_enables_error_recovery_chain(self) -> None: + """Use case: try multiple recovery strategies.""" + + def fetch_primary() -> Result[str, str]: + return Err("primary failed") + + def try_secondary(_e: str) -> Result[str, str]: + return Err("secondary failed") + + def use_default(_e: str) -> Result[str, str]: + return Ok("default value") + + # Chain multiple or_else calls until one succeeds + result = fetch_primary().or_else(try_secondary).or_else(use_default) + assert result.is_ok() + assert result.unwrap() == "default value" + + def test_or_else_function_not_called_on_ok(self) -> None: + """Verify short-circuit behavior - function shouldn't be called for Ok.""" + called = False + + def recovery(_e: str) -> Result[int, str]: + nonlocal called + called = True + return Ok(999) + + res: Result[int, str] = Ok(10) + result = res.or_else(recovery) + assert result.is_ok() + assert result.unwrap() == 10 + assert called is False diff --git a/tests/result/test_conversion.py b/tests/result/test_conversion.py new file mode 100644 index 0000000..809d5f1 --- /dev/null +++ b/tests/result/test_conversion.py @@ -0,0 +1,90 @@ +"""Tests for Result conversion methods (ok, err). + +Note: Type annotations are required when using Ok()/Err() constructors +because they have inferred types Result[T, Never] and Result[Never, E]. +This matches Rust's type system design. Use function return types or +intermediate functions to satisfy strict type checking. +""" + +from __future__ import annotations + +from pyropust import Err, Ok, Result + + +class TestResultOk: + """Test Result.ok() for converting Result to Option.""" + + def test_ok_returns_some_on_ok(self) -> None: + res = Ok(42) + opt = res.ok() + assert opt.is_some() + assert opt.unwrap() == 42 + + def test_ok_returns_none_on_err(self) -> None: + res: Result[int, str] = Err("error") + opt = res.ok() + assert opt.is_none() + + def test_ok_preserves_value_type(self) -> None: + res = Ok({"key": "value"}) + opt = res.ok() + assert opt.unwrap() == {"key": "value"} + + def test_ok_discards_error(self) -> None: + """Verify that error information is lost after ok().""" + res: Result[int, str] = Err("important error") + opt = res.ok() + # Error is discarded, we only know it's None + assert opt.is_none() + + def test_ok_enables_option_chaining(self) -> None: + """Use case: convert Result to Option for further processing.""" + res = Ok(10) + # Chain with Option methods + result = res.ok().map(lambda x: x * 2).unwrap_or(0) + assert result == 20 + + res_err: Result[int, str] = Err("error") + result_err = res_err.ok().map(lambda x: x * 2).unwrap_or(0) + assert result_err == 0 + + +class TestResultErr: + """Test Result.err() for extracting error as Option.""" + + def test_err_returns_some_on_err(self) -> None: + res: Result[int, str] = Err("error message") + opt = res.err() + assert opt.is_some() + assert opt.unwrap() == "error message" + + def test_err_returns_none_on_ok(self) -> None: + res = Ok(42) + opt = res.err() + assert opt.is_none() + + def test_err_preserves_error_type(self) -> None: + error = ValueError("validation failed") + res: Result[int, Exception] = Err(error) + opt = res.err() + assert opt.unwrap() == error + + def test_err_discards_success_value(self) -> None: + """Verify that success value is lost after err().""" + res = Ok(42) + opt = res.err() + # Success value is discarded + assert opt.is_none() + + def test_err_for_error_collection(self) -> None: + """Use case: collect errors from multiple Results.""" + results = [Ok(1), Err("error1"), Ok(3), Err("error2")] + errors = [r.err().unwrap() for r in results if r.is_err()] + assert errors == ["error1", "error2"] + + def test_err_with_option_methods(self) -> None: + """Chain with Option methods for error processing.""" + res: Result[int, str] = Err("bad input") + # Transform error using Option.map + formatted = res.err().map(lambda e: f"Error: {e}").unwrap_or("No error") + assert formatted == "Error: bad input" diff --git a/tests/result/test_extraction.py b/tests/result/test_extraction.py new file mode 100644 index 0000000..75aa6e4 --- /dev/null +++ b/tests/result/test_extraction.py @@ -0,0 +1,119 @@ +"""Tests for Result extraction methods (expect, expect_err, unwrap_or, unwrap_or_else). + +Note: Type annotations are required when using Ok()/Err() constructors +because they have inferred types Result[T, Never] and Result[Never, E]. +This matches Rust's type system design. Use function return types or +intermediate functions to satisfy strict type checking. +""" + +from __future__ import annotations + +import pytest + +from pyropust import Err, Ok, Result + + +class TestResultExpect: + """Test Result.expect() for extracting Ok value with custom error message.""" + + def test_expect_returns_ok_value(self) -> None: + res = Ok(42) + assert res.expect("should not fail") == 42 + + def test_expect_raises_with_custom_message_on_err(self) -> None: + res: Result[int, str] = Err("error") + with pytest.raises(RuntimeError, match="custom error message"): + res.expect("custom error message") + + def test_expect_works_with_complex_types(self) -> None: + res = Ok({"key": "value"}) + assert res.expect("should work") == {"key": "value"} + + def test_expect_message_can_be_multiline(self) -> None: + res: Result[int, str] = Err("error") + with pytest.raises(RuntimeError, match=r"Line 1\nLine 2\nLine 3"): + res.expect("Line 1\nLine 2\nLine 3") + + +class TestResultExpectErr: + """Test Result.expect_err() for extracting Err value with custom error message.""" + + def test_expect_err_returns_err_value(self) -> None: + res: Result[int, str] = Err("error message") + assert res.expect_err("should not fail") == "error message" + + def test_expect_err_raises_with_custom_message_on_ok(self) -> None: + res = Ok(42) + with pytest.raises(RuntimeError, match="expected an error"): + res.expect_err("expected an error") + + def test_expect_err_works_with_exception_objects(self) -> None: + error = ValueError("validation failed") + res: Result[int, Exception] = Err(error) + assert res.expect_err("should work") == error + + +class TestResultUnwrapOr: + """Test Result.unwrap_or() for providing default values.""" + + def test_unwrap_or_returns_ok_value(self) -> None: + res = Ok(10) + assert res.unwrap_or(999) == 10 + + def test_unwrap_or_returns_default_on_err(self) -> None: + res: Result[int, str] = Err("error") + assert res.unwrap_or(999) == 999 + + def test_unwrap_or_works_with_different_types(self) -> None: + # Ok case with string + res = Ok("hello") + assert res.unwrap_or("default") == "hello" + + # Err case with string + res_err: Result[str, str] = Err("error") + assert res_err.unwrap_or("default") == "default" + + def test_unwrap_or_default_can_be_none(self) -> None: + res: Result[str, str] = Err("error") + assert res.unwrap_or(None) is None + + def test_unwrap_or_with_complex_default(self) -> None: + res: Result[list[int], str] = Err("error") + default = [1, 2, 3] + assert res.unwrap_or(default) == [1, 2, 3] + + +class TestResultUnwrapOrElse: + """Test Result.unwrap_or_else() for computing default values from error.""" + + def test_unwrap_or_else_returns_ok_value(self) -> None: + res = Ok(10) + assert res.unwrap_or_else(lambda _e: 999) == 10 + + def test_unwrap_or_else_computes_default_on_err(self) -> None: + res: Result[int, str] = Err("error") + assert res.unwrap_or_else(lambda e: len(e)) == 5 + + def test_unwrap_or_else_receives_err_value(self) -> None: + res: Result[int, str] = Err("custom error") + # Function receives the actual error value + result = res.unwrap_or_else(lambda e: len(e) * 2) + assert result == 24 # len("custom error") * 2 + + def test_unwrap_or_else_not_called_on_ok(self) -> None: + """Verify function is not called when Result is Ok.""" + called = False + + def compute_default(_e: str) -> int: + nonlocal called + called = True + return 999 + + res = Ok(10) + assert res.unwrap_or_else(compute_default) == 10 + assert called is False + + def test_unwrap_or_else_with_type_conversion(self) -> None: + # Error to default value type conversion + res: Result[str, int] = Err(404) + assert res.unwrap_or_else(lambda code: f"Error {code}") == "Error 404" diff --git a/tests/result/test_query.py b/tests/result/test_query.py new file mode 100644 index 0000000..cf46db0 --- /dev/null +++ b/tests/result/test_query.py @@ -0,0 +1,107 @@ +"""Tests for Result query methods (is_ok_and, is_err_and). + +Note: Type annotations are required when using Ok()/Err() constructors +because they have inferred types Result[T, Never] and Result[Never, E]. +This matches Rust's type system design. Use function return types or +intermediate functions to satisfy strict type checking. +""" + +from __future__ import annotations + +from pyropust import Err, Ok, Result + + +class TestResultIsOkAnd: + """Test Result.is_ok_and() for conditional Ok checking.""" + + def test_returns_true_when_ok_and_predicate_true(self) -> None: + res = Ok(10) + assert res.is_ok_and(lambda x: x > 5) is True + + def test_returns_false_when_ok_but_predicate_false(self) -> None: + res = Ok(3) + assert res.is_ok_and(lambda x: x > 5) is False + + def test_returns_false_when_err(self) -> None: + res: Result[int, str] = Err("error") + assert res.is_ok_and(lambda x: x > 5) is False + + def test_accepts_truthy_values(self) -> None: + """Verify Python truthiness protocol works.""" + # Non-empty string is truthy + assert Ok("hello").is_ok_and(lambda x: x) is True + # Empty string is falsy + assert Ok("").is_ok_and(lambda x: x) is False + # Non-zero int is truthy + assert Ok(42).is_ok_and(lambda x: x) is True + # Zero is falsy + assert Ok(0).is_ok_and(lambda x: x) is False + + def test_predicate_receives_ok_value(self) -> None: + """Verify predicate gets the actual Ok value.""" + res = Ok([1, 2, 3]) + assert res.is_ok_and(lambda x: len(x) == 3) is True + assert res.is_ok_and(lambda x: len(x) == 5) is False + + def test_predicate_not_called_when_err(self) -> None: + """Verify short-circuit behavior - predicate shouldn't be called for Err.""" + called = False + + def side_effect_predicate(_x: int) -> bool: + nonlocal called + called = True + return True + + res: Result[int, str] = Err("error") + assert res.is_ok_and(side_effect_predicate) is False + assert called is False + + +class TestResultIsErrAnd: + """Test Result.is_err_and() for conditional Err checking.""" + + def test_returns_true_when_err_and_predicate_true(self) -> None: + res: Result[int, str] = Err("error") + assert res.is_err_and(lambda e: "err" in e) is True + + def test_returns_false_when_err_but_predicate_false(self) -> None: + res: Result[int, str] = Err("success") + assert res.is_err_and(lambda e: "err" in e) is False + + def test_returns_false_when_ok(self) -> None: + res: Result[int, str] = Ok(10) + assert res.is_err_and(lambda e: "err" in e) is False + + def test_accepts_truthy_values(self) -> None: + """Verify Python truthiness protocol works.""" + # Non-empty string is truthy + res_err: Result[int, str] = Err("error") + assert res_err.is_err_and(lambda e: e) is True + # Empty string is falsy + res_empty: Result[int, str] = Err("") + assert res_empty.is_err_and(lambda e: e) is False + + def test_predicate_receives_err_value(self) -> None: + """Verify predicate gets the actual Err value.""" + + class CustomError: + def __init__(self, code: int) -> None: + self.code = code + + err_obj = CustomError(404) + res: Result[int, CustomError] = Err(err_obj) + assert res.is_err_and(lambda e: e.code == 404) is True + assert res.is_err_and(lambda e: e.code == 500) is False + + def test_predicate_not_called_when_ok(self) -> None: + """Verify short-circuit behavior - predicate shouldn't be called for Ok.""" + called = False + + def side_effect_predicate(_e: str) -> bool: + nonlocal called + called = True + return True + + res: Result[int, str] = Ok(10) + assert res.is_err_and(side_effect_predicate) is False + assert called is False diff --git a/tests/result/test_transformation.py b/tests/result/test_transformation.py new file mode 100644 index 0000000..02f0255 --- /dev/null +++ b/tests/result/test_transformation.py @@ -0,0 +1,265 @@ +"""Tests for Result transformation methods. + +Includes: map, map_err, map_or, map_or_else, inspect, inspect_err. + +Note: Type annotations are required when using Ok()/Err() constructors +because they have inferred types Result[T, Never] and Result[Never, E]. +This matches Rust's type system design. Use function return types or +intermediate functions to satisfy strict type checking. +""" + +from __future__ import annotations + +from pyropust import Err, Ok, Result + + +class TestResultMap: + """Test Result.map() for transforming Ok values.""" + + def test_map_transforms_ok_value(self) -> None: + res = Ok(10).map(lambda x: x * 2) + assert res.is_ok() + assert res.unwrap() == 20 + + def test_map_chains_multiple_transforms(self) -> None: + res = Ok("123").map(int).map(lambda x: x * 2) + assert res.is_ok() + assert res.unwrap() == 246 + + def test_map_skips_on_err(self) -> None: + res: Result[int, str] = Err("error").map(lambda x: x * 2) + assert res.is_err() + assert res.unwrap_err() == "error" + + +class TestResultMapErr: + """Test Result.map_err() for transforming Err values.""" + + def test_map_err_transforms_err_value(self) -> None: + res: Result[int, str] = Err("error").map_err(lambda e: e.upper()) + assert res.is_err() + assert res.unwrap_err() == "ERROR" + + def test_map_err_skips_on_ok(self) -> None: + res = Ok(10).map_err(lambda e: f"Transformed: {e}") + assert res.is_ok() + assert res.unwrap() == 10 + + +class TestResultMapOr: + """Test Result.map_or() for transforming with default.""" + + def test_map_or_applies_function_on_ok(self) -> None: + res = Ok(10) + result = res.map_or(0, lambda x: x * 2) + assert result == 20 + + def test_map_or_returns_default_on_err(self) -> None: + res: Result[int, str] = Err("error") + result = res.map_or(0, lambda x: x * 2) + assert result == 0 + + def test_map_or_with_type_conversion(self) -> None: + # Transform int to str on Ok + res = Ok(42) + result = res.map_or("default", lambda x: f"Value: {x}") + assert result == "Value: 42" + + # Use default on Err + res_err: Result[int, str] = Err("error") + result_err = res_err.map_or("default", lambda x: f"Value: {x}") + assert result_err == "default" + + def test_map_or_function_not_called_on_err(self) -> None: + """Verify function is not called when Result is Err.""" + called = False + + def transform(_x: int) -> int: + nonlocal called + called = True + return 999 + + res: Result[int, str] = Err("error") + result = res.map_or(0, transform) + assert result == 0 + assert called is False + + +class TestResultMapOrElse: + """Test Result.map_or_else() for transforming with computed default.""" + + def test_map_or_else_applies_function_on_ok(self) -> None: + res = Ok(10) + result = res.map_or_else(lambda _e: 0, lambda x: x * 2) + assert result == 20 + + def test_map_or_else_computes_default_on_err(self) -> None: + res: Result[int, str] = Err("error") + result = res.map_or_else(lambda e: len(e), lambda x: x * 2) + assert result == 5 + + def test_map_or_else_receives_error_value(self) -> None: + """Verify default function receives the actual error.""" + res: Result[int, int] = Err(404) + result = res.map_or_else(lambda code: code * 10, lambda x: x * 2) + assert result == 4040 + + def test_map_or_else_functions_not_both_called(self) -> None: + """Verify only one function is called.""" + transform_called = False + default_called = False + + def transform(_x: int) -> int: + nonlocal transform_called + transform_called = True + return 999 + + def compute_default(_e: str) -> int: + nonlocal default_called + default_called = True + return 0 + + # Ok case + res = Ok(10) + res.map_or_else(compute_default, transform) + assert transform_called is True + assert default_called is False + + # Err case + transform_called = False + res_err: Result[int, str] = Err("error") + res_err.map_or_else(compute_default, transform) + assert transform_called is False + assert default_called is True + + +class TestResultInspect: + """Test Result.inspect() for side effects.""" + + def test_inspect_calls_function_on_ok(self) -> None: + called_with = None + + def side_effect(x: int) -> None: + nonlocal called_with + called_with = x + + res = Ok(42) + result = res.inspect(side_effect) + assert called_with == 42 + assert result.is_ok() + assert result.unwrap() == 42 + + def test_inspect_not_called_on_err(self) -> None: + called = False + + def side_effect(_x: int) -> None: + nonlocal called + called = True + + res: Result[int, str] = Err("error") + result = res.inspect(side_effect) + assert called is False + assert result.is_err() + assert result.unwrap_err() == "error" + + def test_inspect_enables_chaining(self) -> None: + """Verify inspect returns Result for method chaining.""" + log: list[int] = [] + + res = ( + Ok(10) + .inspect(lambda x: log.append(x)) + .map(lambda x: x * 2) + .inspect(lambda x: log.append(x)) + ) + + assert log == [10, 20] + assert res.unwrap() == 20 + + def test_inspect_preserves_error(self) -> None: + """Verify inspect doesn't affect Err.""" + res: Result[int, str] = Err("error") + result = res.inspect(lambda _x: None).map(lambda x: x * 2) + assert result.is_err() + assert result.unwrap_err() == "error" + + def test_inspect_with_print_debugging(self) -> None: + """Use case: debugging with side effects.""" + values_seen: list[int] = [] + + result = ( + Ok(5) + .inspect(lambda x: values_seen.append(x)) + .map(lambda x: x * 2) + .inspect(lambda x: values_seen.append(x)) + .map(lambda x: x + 1) + .inspect(lambda x: values_seen.append(x)) + ) + + assert values_seen == [5, 10, 11] + assert result.unwrap() == 11 + + +class TestResultInspectErr: + """Test Result.inspect_err() for side effects on errors.""" + + def test_inspect_err_calls_function_on_err(self) -> None: + called_with = None + + def side_effect(e: str) -> None: + nonlocal called_with + called_with = e + + res: Result[int, str] = Err("error") + result = res.inspect_err(side_effect) + assert called_with == "error" + assert result.is_err() + assert result.unwrap_err() == "error" + + def test_inspect_err_not_called_on_ok(self) -> None: + called = False + + def side_effect(_e: str) -> None: + nonlocal called + called = True + + res = Ok(42) + result = res.inspect_err(side_effect) + assert called is False + assert result.is_ok() + assert result.unwrap() == 42 + + def test_inspect_err_enables_chaining(self) -> None: + """Verify inspect_err returns Result for method chaining.""" + log: list[str] = [] + + res: Result[int, str] = ( + Err("error1") + .inspect_err(lambda e: log.append(e)) + .map_err(lambda e: f"{e}_mapped") + .inspect_err(lambda e: log.append(e)) + ) + + assert log == ["error1", "error1_mapped"] + assert res.unwrap_err() == "error1_mapped" + + def test_inspect_err_preserves_ok(self) -> None: + """Verify inspect_err doesn't affect Ok.""" + res = Ok(10) + result = res.inspect_err(lambda _e: None).map(lambda x: x * 2) + assert result.is_ok() + assert result.unwrap() == 20 + + def test_inspect_err_for_error_logging(self) -> None: + """Use case: logging errors without changing the result.""" + errors_logged: list[str] = [] + + result = ( + Err("validation failed") + .inspect_err(lambda e: errors_logged.append(f"Error: {e}")) + .map_err(lambda e: e.upper()) + .inspect_err(lambda e: errors_logged.append(f"Transformed: {e}")) + ) + + assert errors_logged == ["Error: validation failed", "Transformed: VALIDATION FAILED"] + assert result.unwrap_err() == "VALIDATION FAILED" diff --git a/tests/result/test_utility.py b/tests/result/test_utility.py new file mode 100644 index 0000000..25aede7 --- /dev/null +++ b/tests/result/test_utility.py @@ -0,0 +1,137 @@ +"""Tests for Result utility methods (flatten, transpose). + +Note: Type annotations are required when using Ok()/Err() constructors +because they have inferred types Result[T, Never] and Result[Never, E]. +This matches Rust's type system design. Use function return types or +intermediate functions to satisfy strict type checking. +""" + +from __future__ import annotations + +import pytest + +from pyropust import Err, None_, Ok, Option, Result, Some + + +class TestResultFlatten: + """Test Result.flatten() for flattening nested Results.""" + + def test_flatten_ok_ok(self) -> None: + """Flatten Ok(Ok(value)) -> Ok(value).""" + + def make_nested() -> Result[Result[int, str], str]: + return Ok(Ok(42)) + + nested = make_nested() + result = nested.flatten() + assert result.is_ok() + assert result.unwrap() == 42 + + def test_flatten_ok_err(self) -> None: + """Flatten Ok(Err(error)) -> Err(error).""" + + def inner_err() -> Result[int, str]: + return Err("inner error") + + nested: Result[Result[int, str], str] = Ok(inner_err()) + result = nested.flatten() + assert result.is_err() + assert result.unwrap_err() == "inner error" + + def test_flatten_err(self) -> None: + """Flatten Err(error) -> Err(error).""" + nested: Result[Result[int, str], str] = Err("outer error") + result = nested.flatten() + assert result.is_err() + assert result.unwrap_err() == "outer error" + + def test_flatten_requires_nested_result(self) -> None: + """Verify flatten raises TypeError if Ok value is not a Result.""" + res: Result[int, str] = Ok(42) + with pytest.raises(TypeError, match="flatten requires Ok value to be a Result"): + res.flatten() # type: ignore[misc] + + def test_flatten_multiple_levels(self) -> None: + """Verify flatten only removes one level of nesting.""" + + def triple_nested() -> Result[Result[Result[int, str], str], str]: + return Ok(Ok(Ok(42))) + + nested = triple_nested() + result = nested.flatten() + # After one flatten, we have Result[Result[int, str], str] + assert result.is_ok() + inner = result.flatten() + assert inner.is_ok() + assert inner.unwrap() == 42 + + def test_flatten_with_different_error_types_in_layers(self) -> None: + """Verify flatten works when inner and outer error types match.""" + + def make_nested() -> Result[Result[str, int], int]: + # Inner Err with int error type + return Ok(Err(404)) + + nested = make_nested() + result = nested.flatten() + assert result.is_err() + assert result.unwrap_err() == 404 + + +class TestResultTranspose: + """Test Result.transpose() for converting Result[Option[T], E] to Option[Result[T, E]].""" + + def test_transpose_ok_some(self) -> None: + """Transpose Ok(Some(value)) -> Some(Ok(value)).""" + res: Result[Option[int], str] = Ok(Some(42)) + opt = res.transpose() + assert opt.is_some() + inner = opt.unwrap() + assert inner.is_ok() + assert inner.unwrap() == 42 + + def test_transpose_ok_none(self) -> None: + """Transpose Ok(None) -> None.""" + + def make_none() -> Result[Option[int], str]: + return Ok(None_()) + + res = make_none() + opt = res.transpose() + assert opt.is_none() + + def test_transpose_err(self) -> None: + """Transpose Err(error) -> Some(Err(error)).""" + res: Result[Option[int], str] = Err("error") + opt = res.transpose() + assert opt.is_some() + inner = opt.unwrap() + assert inner.is_err() + assert inner.unwrap_err() == "error" + + def test_transpose_requires_option(self) -> None: + """Verify transpose raises TypeError if Ok value is not an Option.""" + res: Result[int, str] = Ok(42) + with pytest.raises(TypeError, match="transpose requires Ok value to be an Option"): + res.transpose() # type: ignore[misc] + + def test_transpose_round_trip_some(self) -> None: + """Verify transpose is self-inverse for Some case.""" + # Option.transpose() not implemented yet, so we manually verify structure + # For now, just verify Result.transpose works correctly + res: Result[Option[int], str] = Ok(Some(42)) + transposed = res.transpose() + assert transposed.is_some() + inner = transposed.unwrap() + assert inner.is_ok() + assert inner.unwrap() == 42 + + def test_transpose_round_trip_none(self) -> None: + """Verify transpose is self-inverse for None case.""" + + def make_none() -> Result[Option[int], str]: + return Ok(None_()) + + res = make_none() + transposed = res.transpose() + assert transposed.is_none() diff --git a/tests/test_direct_usage.py b/tests/test_direct_usage.py deleted file mode 100644 index 177b801..0000000 --- a/tests/test_direct_usage.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Tests for Result and Option direct usage patterns (functional chaining). - -These tests verify the README examples and cover methods not tested elsewhere: -- Result: map(), and_then(), map_err() -- Option: map(), unwrap_or() -""" - -from __future__ import annotations - -from pyropust import Err, None_, Ok, Option, Result, Some - - -class TestResultMap: - """Test Result.map() for transforming Ok values.""" - - def test_map_transforms_ok_value(self) -> None: - res = Ok(10).map(lambda x: x * 2) - assert res.is_ok() - assert res.unwrap() == 20 - - def test_map_chains_multiple_transforms(self) -> None: - res = Ok("123").map(int).map(lambda x: x * 2) - assert res.is_ok() - assert res.unwrap() == 246 - - def test_map_skips_on_err(self) -> None: - res: Result[int, str] = Err("error").map(lambda x: x * 2) - assert res.is_err() - assert res.unwrap_err() == "error" - - -class TestResultAndThen: - """Test Result.and_then() for chaining operations that return Result.""" - - def test_and_then_chains_ok_results(self) -> None: - res = Ok(10).and_then(lambda x: Ok(x * 2)) - assert res.is_ok() - assert res.unwrap() == 20 - - def test_and_then_short_circuits_on_err(self) -> None: - res: Result[int, str] = Err("first error").and_then(lambda x: Ok(x * 2)) - assert res.is_err() - assert res.unwrap_err() == "first error" - - def test_and_then_propagates_inner_err(self) -> None: - def get_ok() -> Result[int, str]: - return Ok(10) - - def inner_err(_val: int) -> Result[int, str]: - return Err("inner error") - - res = get_ok().and_then(inner_err) - assert res.is_err() - assert res.unwrap_err() == "inner error" - - def test_readme_example_functional_chaining(self) -> None: - """Verify the README functional chaining example works.""" - res = Ok("123").map(int).map(lambda x: x * 2).and_then(lambda x: Ok(f"Value is {x}")) - assert res.unwrap() == "Value is 246" - - -class TestResultMapErr: - """Test Result.map_err() for transforming Err values.""" - - def test_map_err_transforms_err_value(self) -> None: - res: Result[int, str] = Err("error").map_err(lambda e: f"wrapped: {e}") - assert res.is_err() - assert res.unwrap_err() == "wrapped: error" - - def test_map_err_skips_on_ok(self) -> None: - res = Ok(10).map_err(lambda e: f"wrapped: {e}") - assert res.is_ok() - assert res.unwrap() == 10 - - -class TestOptionMap: - """Test Option.map() for transforming Some values.""" - - def test_map_transforms_some_value(self) -> None: - opt = Some(10).map(lambda x: x * 2) - assert opt.is_some() - assert opt.unwrap() == 20 - - def test_map_skips_on_none(self) -> None: - opt: Option[int] = None_().map(lambda x: x * 2) - assert opt.is_none() - - -class TestOptionUnwrapOr: - """Test Option.unwrap_or() for providing default values.""" - - def test_unwrap_or_returns_value_on_some(self) -> None: - opt = Some("Alice") - assert opt.unwrap_or("Guest") == "Alice" - - def test_unwrap_or_returns_default_on_none(self) -> None: - opt: Option[str] = None_() - assert opt.unwrap_or("Guest") == "Guest" - - def test_readme_example_option_usage(self) -> None: - """Verify the README Option example works.""" - - def find_user(user_id: int) -> Option[str]: - return Some("Alice") if user_id == 1 else None_() - - # Found user - name_opt = find_user(1) - name = name_opt.unwrap_or("Guest") - assert name == "Alice" - - # Not found user - name_opt2 = find_user(999) - name2 = name_opt2.unwrap_or("Guest") - assert name2 == "Guest" - - -class TestResultManualHandling: - """Test manual Result handling pattern from README.""" - - def test_readme_example_divide_function(self) -> None: - """Verify the README divide example works.""" - - def divide(a: int, b: int) -> Result[float, str]: - if b == 0: - return Err("Division by zero") - return Ok(a / b) - - # Success case - res = divide(10, 2) - assert res.is_ok() - assert res.unwrap() == 5.0 - - # Error case - res_err = divide(10, 0) - assert res_err.is_err() - assert res_err.unwrap_err() == "Division by zero" diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 0000000..092b92f --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,27 @@ +"""Tests for README examples and manual handling patterns.""" + +from __future__ import annotations + +from pyropust import Err, Ok, Result + + +class TestResultManualHandling: + """Test manual Result handling pattern from README.""" + + def test_readme_example_divide_function(self) -> None: + """Verify the README divide example works.""" + + def divide(a: int, b: int) -> Result[float, str]: + if b == 0: + return Err("Division by zero") + return Ok(a / b) + + # Success case + res = divide(10, 2) + assert res.is_ok() + assert res.unwrap() == 5.0 + + # Error case + res_err = divide(10, 0) + assert res_err.is_err() + assert res_err.unwrap_err() == "Division by zero"