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
15 changes: 14 additions & 1 deletion pyropust/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,22 @@ class Result(Generic[T_co, E_co]):
class Option(Generic[T_co]):
def is_some(self) -> bool: ...
def is_none(self) -> bool: ...
def is_some_and(self, predicate: Callable[[T_co], object]) -> bool: ...
def is_none_or(self, predicate: Callable[[T_co], object]) -> bool: ...
def unwrap(self) -> T_co: ...
def map[U](self, f: Callable[[T_co], U]) -> Option[U]: ...
def expect(self, msg: str) -> T_co: ...
def unwrap_or[U](self, default: U) -> T_co | U: ...
def unwrap_or_else[U](self, f: Callable[[], U]) -> T_co | U: ...
def map[U](self, f: Callable[[T_co], U]) -> Option[U]: ...
def map_or[U](self, default: U, f: Callable[[T_co], U]) -> U: ...
def map_or_else[U](self, default_f: Callable[[], U], f: Callable[[T_co], U]) -> U: ...
def inspect(self, f: Callable[[T_co], object]) -> Option[T_co]: ...
def filter(self, predicate: Callable[[T_co], object]) -> Option[T_co]: ...
def and_[U](self, other: Option[U]) -> Option[U]: ...
def and_then[U](self, f: Callable[[T_co], Option[U]]) -> Option[U]: ...
def or_(self, other: Option[T_co]) -> Option[T_co]: ...
def or_else(self, f: Callable[[], Option[T_co]]) -> Option[T_co]: ...
def xor(self, other: Option[T_co]) -> Option[T_co]: ...

class ErrorKind:
InvalidInput: ErrorKind
Expand Down
15 changes: 14 additions & 1 deletion pyropust/pyropust_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,22 @@ class Result(Generic[T_co, E_co]):
class Option(Generic[T_co]):
def is_some(self) -> bool: ...
def is_none(self) -> bool: ...
def is_some_and(self, predicate: Callable[[T_co], object]) -> bool: ...
def is_none_or(self, predicate: Callable[[T_co], object]) -> bool: ...
def unwrap(self) -> T_co: ...
def map[U](self, f: Callable[[T_co], U]) -> Option[U]: ...
def expect(self, msg: str) -> T_co: ...
def unwrap_or[U](self, default: U) -> T_co | U: ...
def unwrap_or_else[U](self, f: Callable[[], U]) -> T_co | U: ...
def map[U](self, f: Callable[[T_co], U]) -> Option[U]: ...
def map_or[U](self, default: U, f: Callable[[T_co], U]) -> U: ...
def map_or_else[U](self, default_f: Callable[[], U], f: Callable[[T_co], U]) -> U: ...
def inspect(self, f: Callable[[T_co], object]) -> Option[T_co]: ...
def filter(self, predicate: Callable[[T_co], object]) -> Option[T_co]: ...
def and_[U](self, other: Option[U]) -> Option[U]: ...
def and_then[U](self, f: Callable[[T_co], Option[U]]) -> Option[U]: ...
def or_(self, other: Option[T_co]) -> Option[T_co]: ...
def or_else(self, f: Callable[[], Option[T_co]]) -> Option[T_co]: ...
def xor(self, other: Option[T_co]) -> Option[T_co]: ...

class ErrorKind:
InvalidInput: ErrorKind
Expand Down
177 changes: 176 additions & 1 deletion src/py/option.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use pyo3::exceptions::PyRuntimeError;
use pyo3::exceptions::{PyRuntimeError, PyTypeError};
use pyo3::prelude::*;

#[pyclass(name = "Option")]
Expand Down Expand Up @@ -42,6 +42,174 @@ impl OptionObj {
Ok(default.clone_ref(py))
}
}

// Query methods
fn is_some_and(&self, py: Python<'_>, predicate: Bound<'_, PyAny>) -> PyResult<bool> {
if self.is_some {
let value = self.value.as_ref().expect("some value");
let result = predicate.call1((value.clone_ref(py),))?;
result.is_truthy()
} else {
Ok(false)
}
}

fn is_none_or(&self, py: Python<'_>, predicate: Bound<'_, PyAny>) -> PyResult<bool> {
if self.is_some {
let value = self.value.as_ref().expect("some value");
let result = predicate.call1((value.clone_ref(py),))?;
result.is_truthy()
} else {
Ok(true)
}
}

// Extraction methods
fn expect(&self, py: Python<'_>, msg: &str) -> PyResult<Py<PyAny>> {
if self.is_some {
Ok(self.value.as_ref().expect("some value").clone_ref(py))
} else {
Err(PyRuntimeError::new_err(msg.to_string()))
}
}

fn unwrap_or_else(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
if self.is_some {
Ok(self.value.as_ref().expect("some value").clone_ref(py))
} else {
let result = f.call0()?;
Ok(result.into())
}
}

// Transformation methods
fn map_or(
&self,
py: Python<'_>,
default: Py<PyAny>,
f: Bound<'_, PyAny>,
) -> PyResult<Py<PyAny>> {
if self.is_some {
let value = self.value.as_ref().expect("some 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_some {
let value = self.value.as_ref().expect("some value");
let result = f.call1((value.clone_ref(py),))?;
Ok(result.into())
} else {
let result = default_f.call0()?;
Ok(result.into())
}
}

fn inspect(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult<Self> {
if self.is_some {
let value = self.value.as_ref().expect("some value");
f.call1((value.clone_ref(py),))?;
}
Ok(OptionObj {
is_some: self.is_some,
value: self.value.as_ref().map(|v| v.clone_ref(py)),
})
}

fn filter(&self, py: Python<'_>, predicate: Bound<'_, PyAny>) -> PyResult<Self> {
if self.is_some {
let value = self.value.as_ref().expect("some value");
let result = predicate.call1((value.clone_ref(py),))?;
if result.is_truthy()? {
Ok(some(value.clone_ref(py)))
} else {
Ok(none_())
}
} else {
Ok(none_())
}
}

// Composition methods
fn and_(&self, py: Python<'_>, other: &Self) -> Self {
if self.is_some {
OptionObj {
is_some: other.is_some,
value: other.value.as_ref().map(|v| v.clone_ref(py)),
}
} else {
none_()
}
}

fn and_then(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult<Self> {
if self.is_some {
let value = self.value.as_ref().expect("some value");
let out = f.call1((value.clone_ref(py),))?;
let option_type = py.get_type::<OptionObj>();
if !out.is_instance(option_type.as_any())? {
return Err(PyTypeError::new_err("and_then callback must return Option"));
}
let out_ref: PyRef<'_, OptionObj> = out.extract()?;
Ok(clone_option(py, &out_ref))
} else {
Ok(none_())
}
}

fn or_(&self, py: Python<'_>, other: &Self) -> Self {
if self.is_some {
OptionObj {
is_some: self.is_some,
value: self.value.as_ref().map(|v| v.clone_ref(py)),
}
} else {
OptionObj {
is_some: other.is_some,
value: other.value.as_ref().map(|v| v.clone_ref(py)),
}
}
}

fn or_else(&self, py: Python<'_>, f: Bound<'_, PyAny>) -> PyResult<Self> {
if self.is_some {
Ok(OptionObj {
is_some: self.is_some,
value: self.value.as_ref().map(|v| v.clone_ref(py)),
})
} else {
let out = f.call0()?;
let option_type = py.get_type::<OptionObj>();
if !out.is_instance(option_type.as_any())? {
return Err(PyTypeError::new_err("or_else callback must return Option"));
}
let out_ref: PyRef<'_, OptionObj> = out.extract()?;
Ok(clone_option(py, &out_ref))
}
}

fn xor(&self, py: Python<'_>, other: &Self) -> Self {
match (self.is_some, other.is_some) {
(true, false) => OptionObj {
is_some: true,
value: self.value.as_ref().map(|v| v.clone_ref(py)),
},
(false, true) => OptionObj {
is_some: true,
value: other.value.as_ref().map(|v| v.clone_ref(py)),
},
_ => none_(),
}
}
}

// Python-facing constructor functions
Expand Down Expand Up @@ -69,3 +237,10 @@ pub fn none_() -> OptionObj {
value: None,
}
}

fn clone_option(py: Python<'_>, out_ref: &PyRef<'_, OptionObj>) -> OptionObj {
OptionObj {
is_some: out_ref.is_some,
value: out_ref.value.as_ref().map(|v| v.clone_ref(py)),
}
}
Loading