from kataria.common import Panic, State
from kataria.iterable import (
    FromIterable,
    Iterable,
    MappingFromIterable,
    NativeIterable,
    OptionSequenceIterable,
    SequenceFromIterable,
    SetFromIterable,
    StringFromIterable,
)
from kataria.option import Option
from kataria.result import Result

__all__ = [
    "Option",
    "Result",
    "Panic",
    "State",
    "Iterable",
    "FromIterable",
    "MappingFromIterable",
    "SequenceFromIterable",
    "SetFromIterable",
    "StringFromIterable",
    "NativeIterable",
    "OptionSequenceIterable",
]

kataria/__init__.py
0x18050e1501/kataria
from typing import Callable, Generic, TypeVar


class Panic(Exception):
    ...


StV = TypeVar("StV")


class State(Generic[StV]):
    def __init__(self, val: StV):
        self.val = val

    def get(self) -> StV:
        return self.val

    def change(self, func: Callable[[StV], StV]) -> StV:
        new = func(self.get())
        return self.set(new)

    def set(self, v: StV) -> StV:
        old = self.get()
        self.val = v
        return old

kataria/common.py
0x18050e1501/kataria
from abc import ABC
from typing import (
    TYPE_CHECKING,
    Callable,
    Container,
    Generic,
    MutableMapping,
    MutableSequence,
    MutableSet,
    TypeVar,
)

from kataria.common import State

if TYPE_CHECKING:
    from kataria.option import Option
    from kataria.result import Result

T = TypeVar("T")
U = TypeVar("U")
St = TypeVar("St", bound=State)
StV = TypeVar("StV")
C = TypeVar("C", bound=Container)
K = TypeVar("K")
V = TypeVar("V")


class FromIterable(Generic[C, T], ABC):
    def __init__(self, collection: C):
        self.collection = collection

    def add(self, item: T):
        return NotImplemented

    def finish(self) -> C:
        return self.collection


class SequenceFromIterable(FromIterable[MutableSequence[T], T]):
    def __init__(self):
        super().__init__(list())

    @classmethod
    def from_existing(cls, lst: MutableSequence[T]):
        return super().__init__(lst)

    def add(self, item: T):
        self.collection.append(item)


class StringFromIterable(FromIterable[str, str]):
    def __init__(self):
        super().__init__("")

    @classmethod
    def from_existing(cls, s: str):
        return super().__init__(s)

    def add(self, item: str):
        self.collection += item


class MappingFromIterable(FromIterable[MutableMapping[K, V], (K, V)]):
    def __init__(self):
        super().__init__(dict())

    @classmethod
    def from_existing(cls, mapping: MutableMapping):
        return super().__init__(mapping)

    def add(self, item: (K, V)):
        k, v = item
        self.collection[k] = v


class SetFromIterable(FromIterable[MutableSet[T], T]):
    def __init__(self):
        super().__init__(set())

    @classmethod
    def from_existing(cls, st: MutableSet[T]):
        return super().__init__(st)

    def add(self, item: T):
        self.collection.add(item)


class Iterable(Generic[T], ABC):
    Item = T

    def next(self) -> "Option[Item]":
        return NotImplemented

    def __iter__(self):
        return self

    def __next__(self):
        item = self.next()
        if item.is_some():
            return item.unwrap()
        raise StopIteration

    def __getitem__(self, item: int) -> "Option[Item]":
        if item < 0:
            raise ValueError("iterators cannot be accessed in reverse")
        return self.nth(item)

    def count(self) -> int:
        i = 0
        while self.next().is_some():
            i += 1
        return i

    def last(self) -> "Option[Item]":
        prv = self.next()
        if prv.is_some():
            while True:
                nxt = self.next()
                if nxt.is_none():
                    break
                prv = nxt

        return prv

    def advance_by(self, n: int) -> "Result[None, int]":
        from kataria.result import Result

        for steps_remaining in range(n, 0, -1):
            item = self.next()
            if item.is_none():
                return Result.Err(steps_remaining)

        return Result.Ok(None)

    def nth(self, n: int) -> "Option[Item]":
        item = self.next()
        for _ in range(n):
            item = self.next()

        return item

    def step_by(self, step: int) -> "StepBy[Item]":
        return StepBy(step, self)

    def chain(self, other: "Iterable[Item]") -> "Chain[Item]":
        return Chain(self, other)

    def zip(self, other: "Iterable") -> "Zip[(T, U)]":
        return Zip(self, other)

    def map(self, op: Callable[[T], U]) -> "Map[U]":
        return Map(op, self)

    def for_each(self, op: Callable[[Item], None]) -> None:
        while (item := self.next()).is_some():
            op(item.unwrap())

    def filter(self, predicate: Callable[[Item], bool]) -> "Filter[Item]":
        return Filter(predicate, self)

    def filter_map(self, filter_map: Callable[[T], "Option[U]"]) -> "FilterMap[U]":
        return FilterMap(filter_map, self)

    def enumerate(self) -> "Enumerate[(int, Item)]":
        return Enumerate(self)

    def peekable(self) -> "Peekable[Item]":
        return Peekable(self)

    def skip_while(self, predicate: Callable[[Item], bool]) -> "SkipWhile[Item]":
        return SkipWhile(predicate, self)

    def take_while(self, predicate: Callable[[Item], bool]) -> "TakeWhile[Item]":
        return TakeWhile(predicate, self)

    def map_while(self, predicate: Callable[[T], "Option[U]"]) -> "MapWhile[U]":
        return MapWhile(predicate, self)

    def skip(self, n: int) -> "Skip[Item]":
        return Skip(n, self)

    def take(self, n: int) -> "Take[Item]":
        return Take(n, self)

    # curse python's nonexplicit mutability
    def scan(self, initial_state: StV, op: Callable[[St, T], "Option[U]"]) -> "Scan[U]":
        return Scan(initial_state, op, self)

    def flat_map(self, op: Callable[[T], U]) -> "FlatMap[U]":
        return FlatMap(op, self)

    def flatten(self) -> "Flatten[Item]":
        return Flatten(self)

    def fuse(self) -> "Fuse[Item]":
        return Fuse(self)

    def inspect(self, op: Callable[[Item], None]) -> "Inspect[Item]":
        return Inspect(op, self)

    def collect(self, into: "FromIterable[C]") -> C:
        for item in self:
            into.add(item)
        return into.finish()

    def partition(
        self,
        predicate: Callable[[Item], bool],
        a: "FromIterable[C]",
        b: "FromIterable[C]",
    ) -> ("FromIterable[C]", "FromIterable[C]"):
        for item in self:
            if predicate(item):
                a.add(item)
            else:
                b.add(item)

        return a.finish(), b.finish()

    def fold(self, init: U, op: Callable[[U, Item], U]) -> U:
        res = init
        for item in self:
            res = op(res, item)
        return res

    def reduce(self, op: Callable[[Item, Item], Item]) -> "Option[Item]":
        if (a := self.next()).is_some():
            a = a.unwrap()
            for b in self:
                a = op(a, b)
        return a

    def all(self, predicate: Callable[[Item], bool]) -> bool:
        return all(self.map(predicate))

    def any(self, predicate: Callable[[Item], bool]) -> bool:
        return any(self.map(predicate))

    def find(self, predicate: Callable[[Item], bool]) -> "Option[Item]":
        from kataria.option import Option

        while (item := self.next()).is_some():
            if predicate(item.unwrap()):
                return item
        return Option.Nothing()

    def find_map(self, predicate: Callable[[Item], "Option[U]"]) -> "Option[U]":
        from kataria.option import Option

        while (item := self.next()).is_some():
            if (mapped := predicate(item.unwrap())).is_some():
                return mapped

        return Option.Nothing()

    def position(self, predicate: Callable[[Item], bool]) -> "Option[int]":
        from kataria.option import Option

        i = 0
        for item in self:
            if predicate(item):
                return Option.Something(i)
            i += 1

        return Option.Nothing()

    def cycle(self) -> "Cycle[Item]":
        return Cycle(self)


class NativeIterable(Iterable[T]):
    Item = T

    def __init__(self, it):
        self._inner = iter(it)

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        try:
            item = self._inner.__next__()
            return Option.Something(item)
        except StopIteration:
            return Option.Nothing()


class OptionSequenceIterable(NativeIterable):
    Item = T

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        try:
            return self._inner.__next__()
        except StopIteration:
            return Option.Nothing()


class StepBy(Iterable):
    Item = Iterable.Item

    def __init__(self, step: int, inner: Iterable):
        if step < 1:
            raise ValueError("step has to be positive")
        self._step = step
        self._inner = inner

    def next(self) -> "Option[Item]":
        out = self._inner.next()
        for _ in range(self._step - 1):
            self._inner.next()

        return out


class Chain(Iterable):
    Item = Iterable.Item

    def __init__(self, a: Iterable[Item], b: Iterable[Item]):
        self._a = a
        self._b = b

    def next(self) -> "Option[Item]":
        a_item = self._a.next()
        if a_item.is_some():
            return a_item
        return self._b.next()


class Zip(Iterable):
    Item = (T, U)

    def __init__(self, a: Iterable[T], b: Iterable[U]):
        self._a = a
        self._b = b

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        a_item = self._a.next()
        b_item = self._b.next()
        if a_item.is_some() and b_item.is_some():
            return Option.Something((a_item.unwrap(), b_item.unwrap()))
        return Option.Nothing()


class Map(Iterable):
    Item = U

    def __init__(self, mapper: Callable[[T], Item], inner: Iterable[T]):
        self._i = inner
        self._op = mapper

    def next(self) -> "Option[Item]":
        return self._i.next().map(self._op)


class Filter(Iterable):
    Item = Iterable.Item

    def __init__(self, predicate: Callable[[Item], bool], inner: Iterable[Item]):
        self._pred = predicate
        self._i = inner

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        while (item := self._i.next()).is_some():
            if self._pred(item.unwrap()):
                return item
        return Option.Nothing()


class FilterMap(Iterable):
    Item = U

    def __init__(self, filter_map: Callable[[T], "Option[Item]"], inner: Iterable[T]):
        self._op = filter_map
        self._i = inner

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        while (item := self._i.next()).is_some():
            if (mapped := self._op(item.unwrap())).is_some():
                return mapped

        return Option.Nothing()


class Enumerate(Iterable):
    Item = Iterable.Item

    def __init__(self, inner: Iterable[Item]):
        self._inner = inner
        self._count = 0

    def next(self) -> "Option[(int, Item)]":
        self._count += 1
        return self._inner.next().map(lambda i: (self._count - 1, i))


class Peekable(Iterable):
    Item = Iterable.Item

    def __init__(self, inner: Iterable[Item]):
        self._inner = inner
        self._cache = self._inner.next()

    def next(self) -> "Option[Item]":
        out = self._cache
        self._cache = self._inner.next()
        return out

    def peek(self) -> "Option[Item]":
        return self._cache


class SkipWhile(Iterable):
    Item = Iterable.Item

    def __init__(self, predicate: Callable[[Item], bool], inner: Iterable[Item]):
        self._inner = inner
        self._pred = predicate
        self._done = False

    def next(self) -> "Option[Item]":
        if self._done:
            return self._inner.next()

        while (item := self._inner.next()).is_some():
            if self._pred(item.unwrap()):
                continue
            self._done = True
            break
        return item


class TakeWhile(Iterable):
    Item = Iterable.Item

    def __init__(self, predicate: Callable[[Item], bool], inner: Iterable[Item]):
        self._inner = inner
        self._pred = predicate
        self._done = False

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        if self._done:
            return Option.Nothing()

        item = self._inner.next()
        if item.is_some_and(self._pred):
            return item

        self._done = True
        return Option.Nothing()


class MapWhile(Iterable):
    Item = U

    def __init__(self, predicate: Callable[[T], "Option[Item]"], inner: Iterable[T]):
        self._inner = inner
        self._pred = predicate
        self._done = False

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        if self._done:
            return Option.Nothing()

        if (item := self._inner.next().and_then(self._pred)).is_some():
            return item

        self._done = True
        return Option.Nothing()


class Skip(Iterable):
    Item = Iterable.Item

    def __init__(self, n: int, inner: Iterable[Item]):
        self._inner = inner

        for _ in range(n):
            self._inner.next()

    def next(self) -> "Option[Item]":
        return self._inner.next()


class Take(Iterable):
    Item = Iterable.Item

    def __init__(self, n: int, inner: Iterable[Item]):
        self._inner = inner
        self._remaining = n

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        if self._remaining <= 0:
            return Option.Nothing()

        self._remaining -= 1

        return self._inner.next()


class Scan(Iterable):
    Item = U

    def __init__(
        self,
        initial_state: StV,
        op: Callable[[St, T], "Option[Item]"],
        inner: Iterable[T],
    ):
        self._inner = inner
        self._op = op
        self._state = State(initial_state)

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        if (a := self._inner.next()).is_some():
            return self._op(self._state, a.unwrap())
        return Option.Nothing()


class FlatMap(Iterable):
    Item = U

    def __init__(self, op: Callable[[T], Iterable[Item]], inner: Iterable[T]):
        self._inner = inner
        self._op = op
        self._current = self._inner.next().map(self._op)

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        if self._current.is_none():
            return Option.Nothing()

        if (item := self._current.unwrap().next()).is_some():
            return item
        else:
            self._current = self._inner.next().map(self._op)
            return self.next()


class Flatten(Iterable):
    Item = Iterable.Item

    def __init__(self, inner: Iterable[Iterable[Item]]):
        self._inner = inner
        self._current = self._inner.next()

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        if self._current.is_none():
            return Option.Nothing()

        if (item := self._current.unwrap().next()).is_some():
            return item
        else:
            self._current = self._inner.next()
            return self.next()


class Fuse(Iterable):
    Item = Iterable.Item

    def __init__(self, inner: Iterable[Item]):
        self._inner = inner
        self._fused = False

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        if self._fused:
            return Option.Nothing()

        if (item := self._inner.next()).is_none():
            self._fused = True
        return item


class Inspect(Iterable):
    Item = Iterable.Item

    def __init__(self, op: Callable[[Item], None], inner: Iterable[Item]):
        self._inner = inner
        self._op = op

    def next(self) -> "Option[Item]":
        return self._inner.next().inspect(self._op)


class Cycle(Iterable):
    Item = Iterable.Item

    def __init__(self, inner: Iterable[Item]):
        self._inner = inner
        self._buf = list()
        self._i = 0
        self._looping = False

    def next(self) -> "Option[Item]":
        from kataria.option import Option

        if self._looping:
            ret = Option.Something(self._buf[self._i])
            self._i = (self._i + 1) % len(self._buf)
            return ret

        if (item := self._inner.next()).is_some():
            self._buf.append(item.unwrap())
            return item

        self._looping = True
        return self.next()

kataria/iterable.py
0x18050e1501/kataria
from __future__ import annotations

from typing import TYPE_CHECKING, Callable, Generic, TypeVar

from kataria import Iterable
from kataria.common import Panic

if TYPE_CHECKING:
    from kataria.result import Result

T = TypeVar("T")
U = TypeVar("U")
E = TypeVar("E")
F = TypeVar("F")
R = TypeVar("R")


class Option(Generic[T], Iterable[T]):
    Item = T

    def __init__(self, is_some: bool, item: T):
        self._inner = item if is_some else None
        self._is_some = is_some

    def __eq__(self, other: "Option[T]"):
        if self.is_some() and other.is_some():
            return self.unwrap() == other.unwrap()
        else:
            return self.is_none() and other.is_none()

    def __repr__(self):
        if self._is_some:
            return f"Option.Some({repr(self._inner)})"
        return "Option.None"

    def __str__(self):
        if self._is_some:
            return f"Option.Some({str(self._inner)})"
        return "Option.None"

    def next(self) -> "Option[Item]":
        return self

    @classmethod
    def Something(cls, value) -> "Option[T]":
        return cls(True, value)

    @classmethod
    def Nothing(cls) -> "Option[T]":
        return cls(False, None)

    def is_some(self) -> bool:
        return self._is_some

    def is_some_and(self, op: Callable[[T], bool]) -> bool:
        return self.is_some() and op(self._inner)

    def is_none(self) -> bool:
        return not self._is_some

    def expect(self, e_msg: str) -> T:
        if self.is_none():
            raise Panic(e_msg)
        return self._inner

    def unwrap(self) -> T:
        if self.is_none():
            raise Panic
        return self._inner

    def unwrap_or(self, default: T) -> T:
        return self._inner if self.is_some() else default

    def unwrap_or_else(self, f: Callable[[], T]) -> T:
        return self._inner if self.is_some() else f()

    def map(self, op: Callable[[T], U]) -> "Option[U]":
        if self.is_some():
            self._inner = op(self._inner)

        return self

    def inspect(self, op: Callable[[T], None]) -> "Option[T]":
        if self.is_some():
            op(self._inner)
        return self

    def map_or(self, default: U, op: Callable[[T], U]) -> U:
        if self.is_some():
            return op(self._inner)
        else:
            return default

    def map_or_else(self, default: Callable[[], U], op: Callable[[T], U]) -> U:
        if self.is_some():
            return op(self._inner)
        else:
            return default()

    def ok_or(self, err: E) -> Result[T, E]:
        from kataria.result import Result

        if self.is_some():
            return Result.Ok(self._inner)
        else:
            return Result.Err(err)

    def ok_or_else(self, op: Callable[[], E]) -> Result[T, E]:
        from kataria.result import Result

        if self.is_some():
            return Result.Ok(self._inner)
        else:
            return Result.Err(op())

    def bool_and(self, other: "Option[T]") -> "Option[T]":
        if self.is_some():
            return other
        else:
            return self

    def and_then(self, op: Callable[[T], "Option[U]"]) -> "Option[U]":
        if self.is_none():
            return self
        else:
            return op(self._inner)

    def filter(self, predicate: Callable[[T], bool]) -> "Option[T]":
        if self.is_some() and predicate(self._inner):
            return self
        return Option.Nothing()

    def bool_or(self, optb: "Option[T]") -> "Option[T]":
        if self.is_some():
            return self
        return optb

    def or_else(self, op: Callable[[], "Option[T]"]) -> "Option[T]":
        if self.is_some():
            return self
        return op()

    def bool_xor(self, optb: "Option[T]") -> "Option[T]":
        if self.is_some() and optb.is_none():
            return self
        elif self.is_none() and optb.is_some():
            return optb
        else:
            return Option.Nothing()

    def replace(self, value: T) -> "Option[T]":
        old = self.__class__(self._is_some, self._inner)
        self._inner = value
        self._is_some = True
        return old

    def zip(self, other: "Option[U]") -> "Option[(T, U)]":
        if self.is_some() and other.is_some():
            return Option.Something((self._inner, other._inner))
        return Option.Nothing()

    def zip_with(self, other: "Option[U]", op: Callable[[T, U], R]) -> "Option[R]":
        if self.is_some() and other.is_some():
            return Option.Something(op(self._inner, other._inner))
        return Option.Nothing()

    def unzip(self: "Option[(T, U)]") -> ("Option[T]", "Option[U]"):
        if self.is_some():
            t, u = self._inner
            return Option.Something(t), Option.Something(u)
        return Option.Nothing(), Option.Nothing()

    def transpose(self: "Option[Result[T, E]]") -> "Result[Option[T], E]":
        from kataria.result import Result

        if self.is_some():
            if self._inner.is_ok():
                return Result.Ok(Option.Something(self._inner.unwrap()))
            return Result.Err(self._inner.unwrap_err())
        return Result.Ok(Option.Nothing())

    def flatten(self: "Option[Option[T]]") -> "Option[T]":
        if self.is_some():
            return self._inner
        return self

kataria/option.py
0x18050e1501/kataria
from __future__ import annotations

from typing import TYPE_CHECKING, Callable, Generic, TypeVar

from kataria.common import Panic
from kataria.iterable import Iterable

if TYPE_CHECKING:
    from kataria.option import Option

T = TypeVar("T")
U = TypeVar("U")
E = TypeVar("E")
F = TypeVar("F")


class Result(Generic[T, E], Iterable[T]):
    Item = T

    def __init__(self, is_ok: bool, value: T | E):
        self._is_ok = is_ok
        self._value = value

    def __eq__(self, other: "Result[T, E]"):
        if self.is_ok() and other.is_ok():
            return self.unwrap() == other.unwrap()
        elif self.is_err() and other.is_err():
            return self.unwrap_err() == other.unwrap_err()
        else:
            return False

    def __repr__(self):
        return f"Result.{'Ok' if self._is_ok else 'Err'}({repr(self._value)})"

    def __str__(self):
        return f"Result.{'Ok' if self._is_ok else 'Err'}({str(self._value)})"

    def next(self) -> "Option[T]":
        return self.ok()

    @classmethod
    def Ok(cls, value: T) -> "Result[T, E]":
        return cls(True, value)

    @classmethod
    def Err(cls, error: E) -> "Result[T, E]":
        return cls(False, error)

    def is_ok(self) -> bool:
        return self._is_ok

    def is_err(self) -> bool:
        return not self._is_ok

    def is_ok_and(self, op: Callable[[T], bool]) -> bool:
        return self.is_ok() and op(self._value)

    def is_err_and(self, op: Callable[[E], bool]) -> bool:
        return self.is_err() and op(self._value)

    def ok(self) -> Option[T]:
        from kataria.option import Option

        return Option(self.is_ok(), self._value)

    def err(self) -> Option[E]:
        from kataria.option import Option

        return Option(self.is_err(), self._value)

    def map(self, op: Callable[[T], U]) -> "Result[U, E]":
        if self.is_ok():
            try:
                self._value = op(self._value)
            except Exception as e:
                return self.Err(e)
        return self

    def map_or(self, default: U, op: Callable[[T], U]) -> U:
        if self.is_err():
            return default
        return op(self._value)

    def map_or_else(self, default: Callable[[E], U], op: Callable[[T], U]) -> U:
        if self.is_err():
            return default(self._value)
        return op(self._value)

    def map_err(self, op: Callable[[E], F]) -> "Result[T, F]":
        if self.is_err():
            self._value = op(self._value)
        return self

    def inspect(self, op: Callable[[T], None]) -> "Result[T, E]":
        if self.is_ok():
            op(self._value)
        return self

    def inspect_err(self, op: Callable[[E], None]) -> "Result[T, E]":
        if self.is_err():
            op(self._value)
        return self

    def expect(self, msg: str) -> T:
        if self.is_ok():
            return self._value
        raise Panic(self._value, msg)

    def unwrap(self) -> T:
        if self.is_ok():
            return self._value
        raise Panic(self._value)

    def expect_err(self, msg: str) -> E:
        if self.is_err():
            return self._value
        raise Panic(self._value, msg)

    def unwrap_err(self) -> E:
        if self.is_err():
            return self._value
        raise Panic(self._value)

    def bool_and(self, other: "Result[U, E]") -> "Result[U, E]":
        if self.is_ok():
            return other
        return self

    def and_then(self, op: Callable[[T], "Result[T, E]"]) -> "Result[T, E]":
        if self.is_err():
            return self

        return op(self._value)

    def bool_or(self, other: "Result[T, F]") -> "Result[T, F]":
        if self.is_ok():
            return self
        return other

    def or_else(self, op: Callable[[E], "Result[T, F]"]) -> "Result[T, F]":
        if self.is_err():
            return op(self._value)
        return self

    def unwrap_or(self, default: T) -> T:
        if self.is_ok():
            return self._value
        return default

    def unwrap_or_else(self, op: Callable[[E], T]) -> T:
        if self.is_ok():
            return self._value
        return op(self._value)

    def transpose(self: "Result[Option[T], E]") -> "Option[Result[T, E]]":
        from kataria.option import Option

        if self.is_ok():
            if self._value.is_none():
                return Option.Nothing()
            return Option.Something(Result.Ok(self._value.unwrap()))
        return Option.Something(Result.Err(self._value))

    def flatten(self: "Result[Result[T, E], E]") -> "Result[T, E]":
        if self.is_err():
            return self
        return self._value

kataria/result.py
0x18050e1501/kataria
import pytest

from kataria import Iterable, NativeIterable, Option, Result

from .utils import CallCounted


@pytest.fixture
def call_counted():
    return CallCounted(lambda *a, **k: None)


@pytest.fixture
def nothing() -> Option:
    return Option.Nothing()


@pytest.fixture
def something() -> Option:
    return Option.Something("meow")


@pytest.fixture
def another_thing() -> Option:
    return Option.Something(42)


@pytest.fixture
def ok() -> Result:
    return Result.Ok("meow")


@pytest.fixture
def err() -> Result:
    return Result.Err("fuck")


@pytest.fixture
def fine() -> Result:
    return Result.Ok("fine")


@pytest.fixture
def finite_iter() -> Iterable:
    return NativeIterable(range(10))


@pytest.fixture
def infinite_iter() -> Iterable:
    def infinite():
        i = 0
        while True:
            yield i
            i += 1

    return NativeIterable(infinite())

tests/fixtures.py
0x18050e1501/kataria
import pytest

from kataria import NativeIterable, Option, Result, SequenceFromIterable
from kataria.common import State
from kataria.iterable import OptionSequenceIterable, StringFromIterable

from .fixtures import call_counted, finite_iter, infinite_iter


def test_count(finite_iter):
    assert finite_iter.count() == 10


def test_collect(finite_iter):
    expected = list(range(10))
    actual = finite_iter.collect(SequenceFromIterable())

    assert expected == actual


def test_last(finite_iter):
    assert finite_iter.last() == Option.Something(9)


def test_advance_by(finite_iter):
    assert finite_iter.advance_by(4) == Result.Ok(None)
    assert finite_iter.next() == Option.Something(4)

    # 3 steps remaining that were empty
    assert finite_iter.advance_by(8) == Result.Err(3)


def test_nth(finite_iter, infinite_iter):
    # nth is 0 indexed
    assert finite_iter.nth(4) == Option.Something(4)
    assert finite_iter.nth(7) == Option.Nothing()
    assert infinite_iter.nth(100) == Option.Something(100)
    assert infinite_iter.nth(100) == Option.Something(201)
    assert infinite_iter[100] == Option.Something(302)
    with pytest.raises(ValueError):
        _ = infinite_iter[-1]


def test_step_by(infinite_iter):
    assert infinite_iter.step_by(2).take(5).collect(SequenceFromIterable()) == [
        0,
        2,
        4,
        6,
        8,
    ]
    with pytest.raises(ValueError):
        _ = infinite_iter.step_by(0)


def test_chain(finite_iter, infinite_iter):
    it_res = finite_iter.chain(infinite_iter).take(15).collect(SequenceFromIterable())

    range_res = list(range(10))
    range_res.extend(range(5))

    assert it_res == range_res


def test_zip(finite_iter, infinite_iter):
    expected = [(i, i) for i in range(10)]
    actual = finite_iter.zip(infinite_iter).collect(SequenceFromIterable())

    assert expected == actual


def test_map(finite_iter):
    expected = [i * 2 for i in range(10)]
    actual = finite_iter.map(lambda v: v * 2).collect(SequenceFromIterable())

    assert expected == actual


def test_for_each(finite_iter, call_counted):
    finite_iter.for_each(call_counted)
    assert call_counted == 10


def test_filter(finite_iter):
    expected = [1, 3, 5, 7, 9]
    actual = finite_iter.filter(lambda v: v % 2 != 0).collect(SequenceFromIterable())

    assert expected == actual


def test_filter_map(finite_iter):
    def filt_map(item):
        if item % 2 == 0:
            return Option.Nothing()
        return Option.Something(item * 2)

    expected = [2, 6, 10, 14, 18]
    actual = finite_iter.filter_map(filt_map).collect(SequenceFromIterable())

    assert expected == actual


def test_enumerate(finite_iter):
    expected = list(enumerate(range(10)))
    actual = finite_iter.enumerate().collect(SequenceFromIterable())

    assert expected == actual


def test_peekable(finite_iter):
    it = finite_iter.peekable()

    assert it.peek() == Option.Something(0)

    it.advance_by(5)

    v = it.peek()
    assert it.peek() == v
    assert v == it.next()
    assert it.peek() == Option.Something(6)


def test_skip_while(finite_iter):
    expected = [7, 8, 9]
    actual = finite_iter.skip_while(lambda v: v < 7).collect(SequenceFromIterable())

    assert expected == actual


def test_take_while(finite_iter):
    expected = [0, 1, 2, 3, 4]
    actual = finite_iter.take_while(lambda v: v < 5).collect(SequenceFromIterable())

    assert expected == actual


def test_map_while(infinite_iter):
    def map_while(item):
        if item < 5:
            return Option.Something(item**2)
        return Option.Nothing()

    expected = [0, 1, 4, 9, 16]
    actual = infinite_iter.map_while(map_while).collect(SequenceFromIterable())

    assert expected == actual


def test_skip(finite_iter):
    expected = list(range(5, 10))
    actual = finite_iter.skip(5).collect(SequenceFromIterable())

    assert expected == actual


def test_take(infinite_iter):
    expected = list(range(20))
    actual = infinite_iter.take(20).collect(SequenceFromIterable())

    assert expected == actual


def test_scan():
    def scanner(state: State, item):
        state.change(lambda v: v * item)

        if state.get() > 6:
            return Option.Nothing()

        return Option.Something(-state.get())

    it = NativeIterable([1, 2, 3, 4]).scan(1, scanner)

    assert it.next() == Option.Something(-1)
    assert it.next() == Option.Something(-2)
    assert it.next() == Option.Something(-6)
    assert it.next() == Option.Nothing()


def test_flat_map():
    words = ["alpha", "beta", "gamma"]

    merged = (
        NativeIterable(words)
        .flat_map(lambda w: NativeIterable(w))
        .collect(StringFromIterable())
    )

    assert merged == "alphabetagamma"


def test_flatten():
    it1 = NativeIterable(range(4))
    it2 = NativeIterable(range(3))

    expected = [0, 1, 2, 3, 0, 1, 2]
    actual = NativeIterable((it1, it2)).flatten().collect(SequenceFromIterable())

    assert expected == actual


def test_fuse():
    expected = list(range(5))

    first = (
        NativeIterable(range(5))
        .map(lambda v: Option.Something(v))
        .collect(SequenceFromIterable())
    )

    second = (
        NativeIterable(range(10, 15))
        .map(lambda v: Option.Something(v))
        .collect(SequenceFromIterable())
    )

    test_iter = OptionSequenceIterable(first + [Option.Nothing()] + second)

    actual = test_iter.fuse().collect(SequenceFromIterable())

    assert expected == actual


def test_inspect(finite_iter, call_counted):
    expected = list(range(10))
    actual = finite_iter.inspect(call_counted).collect(SequenceFromIterable())

    assert expected == actual
    assert call_counted == 10


def test_partition(finite_iter):
    expected_a = [0, 2, 4, 6, 8]
    expected_b = [1, 3, 5, 7, 9]

    actual_a, actual_b = finite_iter.partition(
        lambda v: v % 2 == 0, SequenceFromIterable(), SequenceFromIterable()
    )

    assert expected_a == actual_a
    assert expected_b == actual_b


def test_fold(finite_iter):
    expected = sum(range(10)) + 24
    actual = finite_iter.fold(24, lambda u, t: u + t)

    assert expected == actual


def test_reduce(finite_iter):
    expected = 45
    actual = finite_iter.reduce(lambda a, b: a + b)

    assert expected == actual


def test_all(finite_iter, infinite_iter, call_counted):
    assert finite_iter.all(lambda t: t >= 0)
    assert not infinite_iter.inspect(call_counted).all(lambda t: t < 5)
    assert call_counted == 6


def test_any(finite_iter, infinite_iter, call_counted):
    assert finite_iter.inspect(call_counted).any(lambda t: t == 4)
    assert call_counted == 5


def test_find(finite_iter, infinite_iter):
    def waldo(item):
        return item == 42

    assert finite_iter.find(waldo) == Option.Nothing()
    assert infinite_iter.find(waldo) == Option.Something(42)
    # note: if waldo isn't found on an infinite iterator, this blocks forever.


def test_find_map(finite_iter, infinite_iter):
    def waldo(item):
        if item == 42:
            return Option.Something("the answer")
        return Option.Nothing()

    assert finite_iter.find_map(waldo) == Option.Nothing()
    assert infinite_iter.find_map(waldo) == Option.Something("the answer")


def test_position(finite_iter, infinite_iter):
    def waldo(item):
        return item == 42

    assert finite_iter.position(waldo) == Option.Nothing()
    assert infinite_iter.position(waldo) == Option.Something(42)


def test_cycle(finite_iter):
    expected = list(range(10)) * 3
    actual = finite_iter.cycle().take(30).collect(SequenceFromIterable())

    assert expected == actual

tests/test_iterable.py
0x18050e1501/kataria
import pytest

from kataria import Option, Panic, Result

from .fixtures import another_thing, call_counted, err, nothing, ok, something
from .utils import CallCounted, assert_not_called


def test_Nothing(nothing):
    assert nothing._inner is None
    assert nothing.is_none()


def test_Nothing_unwrap_panic(nothing):
    with pytest.raises(Panic):
        nothing.unwrap()

    with pytest.raises(Panic):
        nothing.expect("fuck")


def test_Something(something):
    assert something.is_some()
    assert something.unwrap() == "meow"


def test_Something_unwrap(something):
    assert something.unwrap() == "meow"
    assert something.expect("fuck") == "meow"


def test_map(something, nothing):
    some = something.map(lambda v: v + v)
    none = nothing.map(assert_not_called)

    assert none == Option.Nothing()
    assert some.is_some()
    assert some.unwrap() == "meowmeow"


def test_is_some_none(something, nothing):
    assert something.is_some()
    assert nothing.is_none()
    assert something.is_some_and(lambda v: v == "meow")


def test_inspect(something, nothing, call_counted):
    nothing.inspect(assert_not_called)
    something.inspect(call_counted)
    assert call_counted.count == 1


def test_map_or(something, nothing):
    def mappy(_):
        return 2

    assert something.map_or(1, mappy) == 2
    assert nothing.map_or(1, mappy) == 1


def test_map_or_else(something, nothing):
    @CallCounted
    def default():
        return 1

    @CallCounted
    def mappy(_):
        return 2

    assert mappy == 0
    assert default == 0

    assert something.map_or_else(default, mappy) == 2
    assert mappy == 1

    assert nothing.map_or_else(default, mappy) == 1
    assert default == 1


def test_ok_or(something, nothing, ok, err):
    some = something.ok_or("fuck")
    none = nothing.ok_or("fuck")

    assert some == ok
    assert none == err


def test_ok_or_else(something, nothing, ok, err):
    some = something.ok_or_else(lambda: "fuck")
    none = nothing.ok_or_else(lambda: "fuck")

    assert some == ok
    assert none == err


def test_bool_and(something, nothing, another_thing):
    assert something.bool_and(another_thing).unwrap() == 42
    assert something.bool_and(nothing).is_none()
    assert nothing.bool_and(something).is_none()


def test_bool_or(something, nothing):
    assert something.bool_or(nothing) == nothing.bool_or(something)
    assert something.bool_or(nothing) == something
    assert nothing.bool_or(nothing).is_none()


def test_bool_xor(something, nothing):
    assert something.bool_xor(nothing) == something
    assert nothing.bool_xor(something) == something
    assert nothing.bool_xor(nothing) == nothing
    assert something.bool_xor(something) == nothing


def test_and_then(something, nothing, call_counted):
    assert nothing.and_then(assert_not_called) == nothing

    something.and_then(call_counted)
    assert call_counted == 1

    meow2 = something.and_then(lambda v: Option.Something(f"{v} {v}"))

    assert meow2.unwrap() == "meow meow"


def test_filter(something, nothing):
    nothing.filter(assert_not_called)

    assert something.filter(lambda _: False) == nothing
    assert something.filter(lambda _: True) == something


def test_or_else(something, nothing):
    assert something.or_else(assert_not_called) == something
    assert nothing.or_else(lambda: Option.Something(42)).unwrap() == 42
    assert nothing.or_else(lambda: Option.Nothing()) == nothing


def test_replace(something, nothing):
    old = something.replace(42)
    assert old.unwrap() == "meow"
    assert something.unwrap() == 42

    old2 = nothing.replace(42)
    assert old2.is_none()
    assert nothing.unwrap() == 42


def test_zip(something, nothing, another_thing):
    assert something.zip(nothing).is_none()
    assert nothing.zip(something).is_none()
    assert something.zip(another_thing).unwrap() == ("meow", 42)


def test_zip_with(something, nothing, another_thing):
    def fmt(a, b):
        return f"{b}, {a}"

    assert something.zip_with(nothing, fmt).is_none()
    assert nothing.zip_with(something, fmt).is_none()
    assert something.zip_with(another_thing, fmt).unwrap() == "42, meow"


def test_unzip(nothing, something):
    a, b = Option.Something(("meow", 42)).unzip()
    assert a.unwrap() == "meow"
    assert b.unwrap() == 42

    c, d = nothing.unzip()
    assert c.is_none()
    assert d.is_none()

    with pytest.raises(ValueError):
        something.unzip()

    with pytest.raises(TypeError):
        b.unzip()


def test_transpose(ok, err, nothing):
    some_ok = Option.Something(ok)
    some_err = Option.Something(err)

    assert nothing.transpose() == Result.Ok(nothing)
    assert some_ok.transpose() == Result.Ok(Option.Something("meow"))
    assert some_err.transpose() == Result.Err("fuck")


def test_flatten(something, nothing):
    nesty = Option.Something(something)
    assert nesty.flatten() == something
    assert nothing.flatten() == nothing

tests/test_option.py
0x18050e1501/kataria
import pytest

from kataria import Option, Panic, Result

from .fixtures import call_counted, err, fine, nothing, ok, something
from .utils import assert_not_called


def test_ok(ok):
    assert ok.is_ok()
    assert ok.is_ok_and(lambda v: v == "meow")
    assert not ok.is_err()
    assert not ok.is_err_and(assert_not_called)
    assert ok._value == "meow"


def test_err(err):
    assert err.is_err()
    assert err.is_err_and(lambda e: e == "fuck")
    assert not err.is_ok()
    assert not err.is_ok_and(assert_not_called)
    assert err._value == "fuck"


def test_ok_fn(ok, err, something, nothing):
    assert ok.ok() == something
    assert err.ok() == nothing


def test_err_fn(ok, err, nothing):
    something = Option.Something("fuck")

    assert ok.err() == nothing
    assert err.err() == something


def test_unwrap(ok, err):
    assert ok.unwrap() == "meow"
    with pytest.raises(Panic):
        err.unwrap()

    assert ok.expect("heck") == "meow"
    with pytest.raises(Panic):
        try:
            err.expect("heck")
        except Panic as e:
            assert "heck" in f"{e}"
            raise e


def test_unwrap_err(ok, err):
    with pytest.raises(Panic):
        ok.unwrap_err()
    assert err.unwrap_err() == "fuck"

    with pytest.raises(Panic):
        try:
            ok.expect_err("heck")
        except Panic as e:
            assert "heck" in f"{e}"
            raise e
    assert err.expect_err("heck") == "fuck"


def test_unwrap_or(ok, err):
    assert ok.unwrap_or(42) == "meow"
    assert err.unwrap_or(42) == 42


def test_unwrap_or_else(ok, err):
    assert ok.unwrap_or_else(assert_not_called) == "meow"
    assert err.unwrap_or_else(lambda e: f"{e}") == "fuck"


def test_map(ok, err):
    assert err.map(assert_not_called) == err
    assert ok.map(lambda v: v * 2).unwrap() == "meowmeow"


def test_map_or(ok, err):
    assert ok.map_or(42, lambda v: v * 2) == "meowmeow"
    assert err.map_or(42, assert_not_called) == 42


def test_map_or_else(ok, err):
    def get_default(e):
        return "fuck" in f"{e}"

    assert ok.map_or_else(get_default, lambda v: v * 2) == "meowmeow"
    assert err.map_or_else(get_default, assert_not_called)


def test_map_err(ok, err):
    assert ok.map_err(assert_not_called).unwrap() == "meow"
    assert err.map_err(lambda e: 42).unwrap_err() == 42


def test_inspect(ok, err, call_counted):
    ok.inspect(call_counted)
    err.inspect(assert_not_called)
    assert call_counted == 1

    ok.inspect_err(assert_not_called)
    err.inspect_err(call_counted)
    assert call_counted == 2


def test_bool_and(ok, err, fine):
    assert ok.bool_and(fine) == fine
    assert ok.bool_and(err) == err
    assert err.bool_and(ok) == err


def test_bool_or(ok, err, fine):
    assert ok.bool_or(fine) == ok
    assert err.bool_or(fine) == fine
    assert ok.bool_or(err) == ok
    assert err.bool_or(err) == err


def test_and_then(ok, err, fine):
    assert err.and_then(assert_not_called) == err
    assert ok.and_then(lambda t: fine) == fine
    assert ok.and_then(lambda t: err) == err


def test_or_else(ok, err):
    assert ok.or_else(assert_not_called) == ok
    assert err.or_else(lambda e: Result.Ok(e)).unwrap() == "fuck"


def test_transpose(err, something, nothing):
    ok_some = Result.Ok(something)
    ok_none = Result.Ok(nothing)

    assert ok_none.transpose() == nothing
    assert ok_some.transpose() == Option.Something(Result.Ok("meow"))
    assert err.transpose() == Option.Something(Result.Err("fuck"))


def test_flatten(ok, err):
    nesty = Result.Ok(ok)
    nesty2 = Result.Ok(err)

    assert nesty.flatten() == ok
    assert nesty2.flatten() == err
    assert err.flatten() == err

tests/test_result.py
0x18050e1501/kataria
from functools import total_ordering

from kataria import Panic


@total_ordering
class CallCounted:
    def __init__(self, func):
        self.f = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        return self.f(*args, **kwargs)

    def __eq__(self, other):
        return self.count == other

    def __lt__(self, other):
        return self.count < other

    def __repr__(self):
        return f"CallCounted({self.count})"


def assert_not_called():
    raise Panic

tests/utils.py
0x18050e1501/kataria