Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions pyropust/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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](
Expand Down
17 changes: 17 additions & 0 deletions pyropust/pyropust_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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](
Expand Down
220 changes: 220 additions & 0 deletions src/py/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -38,6 +39,56 @@ impl ResultObj {
}
}

fn expect(&self, py: Python<'_>, msg: &str) -> PyResult<Py<PyAny>> {
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<Py<PyAny>> {
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<PyAny>) -> Py<PyAny> {
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<Py<PyAny>> {
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<Self> {
if self.is_ok {
let value = self.ok.as_ref().expect("ok value");
Expand All @@ -58,6 +109,113 @@ impl ResultObj {
}
}

fn map_or(
&self,
py: Python<'_>,
default: Py<PyAny>,
f: Bound<'_, PyAny>,
) -> PyResult<Py<PyAny>> {
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<Py<PyAny>> {
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<Self> {
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<Self> {
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<Self> {
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::<ResultObj>();
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<Self> {
if self.is_ok {
let value = self.ok.as_ref().expect("ok value");
Expand All @@ -73,6 +231,68 @@ impl ResultObj {
}
}

fn is_ok_and(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult<bool> {
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<bool> {
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<Self> {
if self.is_ok {
let value = self.ok.as_ref().expect("ok value");
let result_type = py.get_type::<ResultObj>();
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<OptionObj> {
if self.is_ok {
let value = self.ok.as_ref().expect("ok value");
let option_type = py.get_type::<OptionObj>();
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(
Expand Down
Empty file added tests/option/__init__.py
Empty file.
46 changes: 46 additions & 0 deletions tests/option/test_methods.py
Original file line number Diff line number Diff line change
@@ -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"
Empty file added tests/result/__init__.py
Empty file.
Loading