Source code for cans

from dataclasses import dataclass
from typing import (
    Any,
    Callable,
    Generic,
    Iterator,
    Optional,
    Sequence,
    Tuple,
    TypeVar,
    Union,
    overload,
)

from typing_extensions import final

# Single-sourcing the version number with poetry:
# https://github.com/python-poetry/poetry/pull/2366#issuecomment-652418094
try:
    __version__ = __import__("importlib.metadata").metadata.version(__name__)
except ModuleNotFoundError:  # pragma: no cover
    __version__ = __import__("importlib_metadata").version(__name__)

__all__ = [
    "Just",
    "Maybe",
    "Nothing",
    "flatten",
    "maybe_from_optional",
]

T = TypeVar("T", covariant=True)
U = TypeVar("U", covariant=True)
V = TypeVar("V")


# Why not the Rust-like Option(Some/None) terminology?
#     (1) it's confusing with Optional
#     (2) `None` is reserved in Python
[docs]class _MaybeMixin(Generic[T], Sequence[T]): """Collection of methods supported by both :class:`~cans.Just` and :class:`~cans.Nothing`. """ __slots__ = () def __init__(self) -> None: raise TypeError( "Can't instantiate Maybe directly. " "Use the factory methods `of` and `from_optional` instead." )
[docs] def unwrap(self) -> T: """Unwrap the value in this container. If there is no value, :class:`TypeError` is raised. Example ------- >>> Just(6).unwrap() 6 >>> Nothing().unwrap() Exception: TypeError() Tip --- Only use this if you're absolutely sure that there is a value. If not, use :meth:`~cans._MaybeMixin.unwrap_or` instead. Or, use :meth:`~cans._MaybeMixin.expect` for a more descriptive message. """ raise NotImplementedError()
[docs] def unwrap_or(self, _default: V) -> Union[T, V]: """Unwrap the value in this container. If there is no value, return the given default. Example ------- >>> Just(8).unwrap_or("foo") 8 >>> Nothing().unwrap_or("foo") "foo" """ raise NotImplementedError()
[docs] def expect(self, _msg: str) -> T: """Unwrap the value in this container. If there is no value, raise an AssertionError with message >>> Just(9).expect("What on earth?") 9 >>> Nothing().expect("What did you expect?") Exception: AssertionError("What did you expect?") """ raise NotImplementedError()
[docs] def is_just(self) -> bool: """True if this `Maybe` contains a value Example ------- >>> Just(2).is_just() True >>> Nothing().is_just() False """ raise NotImplementedError()
[docs] def is_nothing(self) -> bool: """True if this `Maybe` does not contain a value Example ------- >>> Just(2).is_nothing() False >>> Nothing().is_nothing() True """ raise NotImplementedError()
[docs] def map(self, _func: Callable[[T], U]) -> "Maybe[U]": """Apply a function to the value inside the `Maybe`, without unwrapping it. Example ------- >>> Just("hello").map(str.upper) Just("HELLO") >>> Nothing().map(abs) Nothing() """ raise NotImplementedError()
[docs] def filter(self, _func: Callable[[T], bool]) -> "Maybe[T]": """Keep the value inside only if it satisfies the given predicate. Example ------- >>> Just("9").filter(str.isdigit) Just("9") >>> Just("foo").filter(str.isdigit) Nothing() >>> Nothing().filter(str.isdigit) Nothing() """ raise NotImplementedError()
[docs] def zip(self, _other: "Maybe[U]") -> "Maybe[Tuple[T, U]]": """Combine two values in a tuple, if both are present. Example ------- >>> Just(8).zip(Just(2)) Just((8, 2)) >>> Just(7).zip(Nothing()) Nothing() >>> Nothing().zip(Just(3)) Nothing() >>> Nothing().zip(Nothing()) Nothing() """ raise NotImplementedError()
[docs] def flatmap(self, _func: Callable[[T], "Maybe[U]"]) -> "Maybe[U]": """Apply a function (which returns a maybe) to the value inside the ``Maybe``. Then, flatten the result. Example ------- >>> def first(s) -> Maybe: ... try: ... return Just(s[0]) ... except LookupError: ... return Nothing() ... >>> Just([9, 4]).flatmap(first) Just(9) >>> Just([]).flatmap(first) Nothing() >>> Nothing().flatmap(first) Nothing() """ raise NotImplementedError()
[docs] def as_optional(self) -> Optional[T]: """Convert the value into an possibly-None value. Example ------- >>> Just(6).as_optional() 6 >>> Nothing().as_optional() None """ raise NotImplementedError()
[docs] def setdefault(self, _v: V) -> "Maybe[Union[T, V]]": """Set a value if one is not already present. Example ------- >>> Just(6).setdefault(7) Just(6) >>> Nothing().setdefault(3) Just(3) """ raise NotImplementedError()
[docs] def and_(self, _other: "Maybe[U]") -> "Maybe[Union[T, U]]": """ Perform a logical AND operation. Returns the first Nothing, or the last of the two values. Tip --- Available as the :meth:`~cans._MaybeMixin.and_` method as well as the ``&`` operator. Example ------- >>> Just(5) & Just(9) Just(9) >>> Just(5).and_(Just(9)) Just(9) >>> Just(9) & Nothing() Nothing() >>> Nothing() & Just(8) Nothing() """ raise NotImplementedError()
__and__ = and_
[docs] def or_(self, _other: "Maybe[U]") -> "Maybe[Union[T, U]]": """Perform a logical OR operation. Return the first Just, or the last of the two values. Tip --- Available as the :meth:`~cans._MaybeMixin.or_` method as well as the ``|`` operator. Example ------- >>> Just(5) | Just(9) Just(5) >>> Just(5).or_(Just(9)) Just(5) >>> Just(9) | Nothing() Just(9) >>> Nothing() | Just(8) Just(8) >>> Nothing() | Nothing() Nothing() """ raise NotImplementedError()
__or__ = or_
[docs] def __iter__(self) -> Iterator[T]: """Iterate over the contained item, if present. Example ------- >>> list(Just(5)) [5] >>> list(Nothing()) [] """ raise NotImplementedError()
[docs] def __contains__(self, _v: object) -> bool: """Check if the item is contained in this object. Example ------- >>> 5 in Just(5) True >>> 4 in Just(8) False >>> 1 in Nothing() False """ raise NotImplementedError()
[docs] def __len__(self) -> int: """The number of items contained (0 or 1) Example ------- >>> len(Just(5)) 1 >>> len(Nothing()) 0 """ raise NotImplementedError()
@overload def __getitem__(self, _i: int) -> T: ... @overload def __getitem__(self, _i: slice) -> "Maybe[T]": ...
[docs] def __getitem__(self, _i: Union[int, slice]) -> Union[T, "Maybe[T]"]: """Get the item from this container by index. Part of the :class:`~collections.abc.Sequence` API. Behaves similarly to a list of one or zero items. Example ------- >>> Just(6)[0] 6 >>> Just(8)[8] Exception: IndexError >>> Nothing()[0] Exception: IndexError ... >>> Just(6)[:] Just(6) >>> Just(2)[:9:4] Just(2) >>> Just(7)[2:] Nothing() >>> Nothing()[:] Nothing() >>> Nothing()[2:9:5] Nothing() """ raise NotImplementedError()
[docs]@final @dataclass(frozen=True, repr=False) class Just(_MaybeMixin[T]): """The version of :class:`~cans.Maybe` which contains a value. Example ------- >>> a: Maybe[int] = Just(-8) >>> a.map(abs).unwrap() 8 """ __slots__ = ("_value",) _value: T def unwrap(self) -> T: return self._value def unwrap_or(self, _default: V) -> Union[T, V]: return self._value def expect(self, _msg: str) -> T: return self._value def is_just(self) -> bool: return True def is_nothing(self) -> bool: return False def map(self, _func: Callable[[T], U]) -> "Maybe[U]": return Just(_func(self._value)) def filter(self, _func: Callable[[T], bool]) -> "Maybe[T]": return self if _func(self._value) else _NOTHING def zip(self, _other: "Maybe[U]") -> "Maybe[Tuple[T, U]]": return ( Just((self._value, _other.unwrap())) if _other.is_just() else _NOTHING ) def flatmap(self, _func: Callable[[T], "Maybe[U]"]) -> "Maybe[U]": return flatten(self.map(_func)) def as_optional(self) -> Optional[T]: return self._value def setdefault(self, _v: V) -> "Maybe[Union[T, V]]": return self def and_(self, _other: "Maybe[U]") -> "Maybe[Union[T, U]]": return _other __and__ = and_ def or_(self, _other: "Maybe[U]") -> "Maybe[Union[T, U]]": return self __or__ = or_ def __repr__(self) -> str: return f"Just({self._value})" def __iter__(self) -> Iterator[T]: yield self._value def __contains__(self, _v: object) -> bool: return self._value == _v def __len__(self) -> int: return 1 def __bool__(self) -> bool: return True def __getstate__(self) -> tuple: return (self._value,) def __setstate__(self, state: tuple) -> None: object.__setattr__(self, "_value", state[0]) @overload def __getitem__(self, _i: int) -> T: ... @overload def __getitem__(self, _i: slice) -> "Maybe[T]": ... def __getitem__(self, _i: Union[int, slice]) -> Union[T, "Maybe[T]"]: if isinstance(_i, slice): return self if _i.indices(1)[:2] == (0, 1) else _NOTHING elif _i == 0: return self._value else: raise IndexError("Only index 0 can be retrieved from container.")
[docs]@final @dataclass(frozen=True) class Nothing(_MaybeMixin[T]): """The version of :class:`~cans.Maybe` which does not contain a value. Example ------- >>> a: Maybe[int] = Nothing() >>> a.map(abs).unwrap_or("nope") "nope" """ __slots__ = () def unwrap(self) -> T: raise TypeError("Cannot unwrap an empty `Maybe`.") def unwrap_or(self, _default: V) -> Union[T, V]: return _default def expect(self, _msg: str) -> T: raise AssertionError(_msg) def is_just(self) -> bool: return False def is_nothing(self) -> bool: return True def map(self, _func: Callable[[T], U]) -> "Maybe[U]": return _NOTHING def filter(self, _func: Callable[[T], bool]) -> "Maybe[T]": return _NOTHING def zip(self, _other: "Maybe[U]") -> "Maybe[Tuple[T, U]]": return _NOTHING def flatmap(self, _func: Callable[[T], "Maybe[U]"]) -> "Maybe[U]": return _NOTHING def as_optional(self) -> Optional[T]: return None def setdefault(self, _v: V) -> "Maybe[Union[T, V]]": return Just(_v) def and_(self, _other: "Maybe[U]") -> "Maybe[Union[T, U]]": return _NOTHING __and__ = and_ def or_(self, _other: "Maybe[U]") -> "Maybe[Union[T, U]]": return _other __or__ = or_ def __iter__(self) -> Iterator[T]: return _EMPTY_ITERATOR def __contains__(self, _v: Any) -> bool: return False def __len__(self) -> int: return 0 def __bool__(self) -> bool: return False def __getstate__(self) -> tuple: return () def __setstate__(self, state: tuple) -> None: pass @overload def __getitem__(self, _i: int) -> T: ... @overload def __getitem__(self, _i: slice) -> "Maybe[T]": ... def __getitem__(self, _i: Union[int, slice]) -> Union[T, "Maybe[T]"]: if isinstance(_i, slice): return _NOTHING else: raise IndexError("No items in this container.")
# Q: Why declare this as a union, and not a base class? # A: So that mypy can properly infer that there are only two variants, # and thus handle conditionals and pattern matching accordingly. Maybe = Union[Just[T], Nothing[T]] """ A container which contains either one item, or none. Use :class:`~cans.Just` and :class:`~cans.Nothing` to express these two variants. When type annotating, only use :data:`~cans.Maybe`. >>> a: Maybe[int] = Just(5) >>> b: Maybe[str] = Nothing() ... >>> def parse(s: str) -> Maybe[int]: ... try: return Just(int(s)) ... except ValueError: return Nothing() ... >>> def first(m: list[T]) -> Maybe[T]: ... return Just(m[0]) if m else Nothing() ... >>> parse("42") Just(42) >>> first([]) Nothing() Various methods are available (documented in :class:`~cans._MaybeMixin`) which you can use to operate on the value, without repeatedly unpacking it. >>> first(["-5", "6", ""]).flatmap(parse).map(abs) Just(5) >>> first([]).flatmap(parse).map(abs).unwrap_or("Nothing here...") "Nothing here..." In Python 3.10+, you can use pattern matching to deconstruct a :class:`~cans.Maybe`. >>> match first(["bob", "henry", "anita"]).map(str.title): ... case Just(x): ... print(f'Hello {x}!') ... case Nothing() ... print('Nobody here...') "Hello Bob!" :class:`~cans.Maybe` also implements the :class:`~collections.abc.Sequence` API, meaning it acts kind of like a list with one or no items. Thus, it works nicely with :mod:`itertools`! >>> from itertools import chain >>> list(chain.from_iterable(map(parse, "a4f59b"))) [4, 5, 9] """ _NOTHING: Maybe = Nothing() _EMPTY_ITERATOR: Iterator = iter(())
[docs]def flatten(m: "Maybe[Maybe[T]]") -> "Maybe[T]": """Flatten two nested maybes. Example ------- >>> flatten(Just(Just(5))) Just(5) >>> flatten(Just(Nothing())) Nothing() >>> flatten(Nothing()) Nothing() """ return m.unwrap() if m.is_just() else m # type: ignore
[docs]def maybe_from_optional(_v: Optional[T]) -> "Maybe[T]": """Create a maybe container from the given value, which may be ``None``. In that case, it'll be an empty Maybe. Example ------- >>> maybe_from_optional(5) Just(5) >>> maybe_from_optional(None) Nothing() """ return _NOTHING if _v is None else Just(_v)