diff --git a/README.md b/README.md index 6a36500..7a6678c 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,18 @@ Alternatively the map method can be used to return a new type instance with the │ ├── ci_tests.yaml │ └── publish.yaml ├── dev_tools +│ ├── create_examples +│ │ ├── collection +│ │ │ ├── __init__.py +│ │ │ ├── example.py +│ │ │ ├── record.py +│ │ │ └── recorder.py # User entrypoint `example` and it's inner class `Example` +│ │ ├── transformation +│ │ │ ├── __init__.py +│ │ │ ├── ast_editing.py +│ │ │ ├── format_examples.py +│ │ │ └── transform.py +│ │ └── __init__.py │ ├── __init__.py │ ├── update_cov.py │ └── update_readme.py @@ -268,26 +280,26 @@ Alternatively the map method can be used to return a new type instance with the │ └── danom │ ├── _monads │ │ ├── __init__.py -│ │ ├── _either.py # A simple Either monad, includes the base Either, Right and Left. +│ │ ├── _either.py # A simple Either monad, includes the base Either, Right and Left. │ │ ├── _option.py -│ │ ├── _result.py # A simple Result monad, includes the base Result, Ok and Err. -│ │ └── _safe.py # decorators that except given exception types and return a monad of the result +│ │ ├── _result_v2.py +│ │ └── _safe.py # decorators that except given exception types and return a monad of the result │ ├── _stream │ │ ├── __init__.py │ │ ├── _async.py -│ │ ├── _base.py # the base class for Stream +│ │ ├── _base.py # the base class for Stream │ │ ├── _par.py │ │ └── _sync.py │ ├── __init__.py -│ ├── _new_type.py # function to create a new type, probably worth deprecating soon -│ └── _utils.py # random junk I can't think of place to put. compose, all_of, any_of, etc +│ ├── _new_type.py # function to create a new type, probably worth deprecating soon +│ └── _utils.py # random junk I can't think of place to put. compose, all_of, any_of, etc ├── tests │ ├── monads │ │ ├── __init__.py │ │ ├── test_either.py │ │ ├── test_monad_laws.py │ │ ├── test_option.py -│ │ └── test_result.py +│ │ └── test_result_v2.py │ ├── stream │ │ ├── __init__.py │ │ ├── _common.py diff --git a/dev_tools/create_examples/__init__.py b/dev_tools/create_examples/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dev_tools/create_examples/collection/__init__.py b/dev_tools/create_examples/collection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dev_tools/create_examples/collection/example.py b/dev_tools/create_examples/collection/example.py new file mode 100644 index 0000000..b3dce27 --- /dev/null +++ b/dev_tools/create_examples/collection/example.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +import attrs + +from .record import ExampleRecord +from .recorder import _RECORDER, Recorder + + +@attrs.define(frozen=True) +class Example[T]: + fn: Callable + args: tuple[Any, ...] + kwargs: dict[str, Any] + actual_result: T + recorder: Recorder + description: str | None = None + + def __eq__(self, expected: T) -> bool: + self.recorder.record_example( + ExampleRecord.new( + self.fn, + self.args, + self.kwargs, + returned=self.actual_result, + expected=expected, + description=self.description, + ) + ) + return self.actual_result == expected + + def __hash__(self) -> int: + hash_value = "".join( + [ + self.fn.__qualname__, + self.fn.__module__, + str(self.args), + str(self.kwargs), + str(self.actual_result), + ] + ) + return hash(hash_value) + + +def example( + fn: Callable, *args: tuple[Any, ...], description: str | None = None, **kwargs: dict[str, Any] +) -> Example: + value = fn(*args, **kwargs) + return Example( + fn, args, kwargs, actual_result=value, description=description, recorder=_RECORDER + ) diff --git a/dev_tools/create_examples/collection/record.py b/dev_tools/create_examples/collection/record.py new file mode 100644 index 0000000..cc26fd5 --- /dev/null +++ b/dev_tools/create_examples/collection/record.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import inspect +from collections.abc import Callable +from pathlib import Path +from typing import Any, Self + +import attrs + + +@attrs.define(frozen=True, eq=True) +class ExampleRecord: + cls_name: str + fn_name: str + module: str + src_file: str + ent_repr: str + args: tuple[Any, ...] + kwargs: dict[str, Any] + returned: Any + expected: Any + description: str | None = None + + @classmethod + def new( # noqa: PLR0913 + cls, + fn: Callable, + args: tuple[Any, ...], + kwargs: dict[str, Any], + returned: Any, # noqa: ANN401 + expected: Any, # noqa: ANN401 + description: str | None = None, + ) -> Self: + return cls( + cls_name=fn.__self__.__class__.__name__, + fn_name=fn.__name__, + module=fn.__module__, + src_file=str(Path(inspect.getsourcefile(fn))), + ent_repr=f"{fn.__self__!r}.{fn.__name__}" + if fn.__class__.__name__ == "method" + else fn.__name__, + args=tuple(_get_repr(arg) for arg in args), + kwargs={k: _get_repr(v) for k, v in kwargs.items()}, + returned=_get_repr(returned), + expected=_get_repr(expected), + description=description, + ) + + def to_dict(self) -> dict[str, str]: + return attrs.asdict(self) + + +def _get_repr(arg: Any) -> str: # noqa: ANN401 + return _clean_repr(repr(arg)) + + +def _clean_repr(raw: str) -> str: + if raw.startswith("").split(".")[-1] + if raw.startswith("") + return raw diff --git a/dev_tools/create_examples/collection/recorder.py b/dev_tools/create_examples/collection/recorder.py new file mode 100644 index 0000000..133364e --- /dev/null +++ b/dev_tools/create_examples/collection/recorder.py @@ -0,0 +1,42 @@ +"""repo-map-desc: User entrypoint `example` and it's inner class `Example` + +The equality operator is where the magic happens +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Self + +import attrs + +from .record import ExampleRecord + + +@attrs.define +class Recorder: + path: Path = Path("./.papertrail_cache/examples.json") + records: list[ExampleRecord] = attrs.field(factory=list) + files: dict[Path, str] = attrs.field(factory=dict) + + def __attrs_post_init__(self) -> None: + self.path = Path(self.path) + + def record_example(self, example: ExampleRecord) -> Self: + self.records.append(example) + return self + + def prepare_files(self) -> Self: + self.files[self.path] = json.dumps([r.to_dict() for r in self.records], indent=2) + self.files[self.path.parent / ".gitignore"] = "# automatically created by papertrail\n*" + return self + + def write_examples(self) -> Self: + for path, data in self.files.items(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(data) + return self + + +_RECORDER = Recorder() diff --git a/dev_tools/create_examples/transformation/__init__.py b/dev_tools/create_examples/transformation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dev_tools/create_examples/transformation/ast_editing.py b/dev_tools/create_examples/transformation/ast_editing.py new file mode 100644 index 0000000..41a2be2 --- /dev/null +++ b/dev_tools/create_examples/transformation/ast_editing.py @@ -0,0 +1,97 @@ +import re +import textwrap + +import libcst as cst + + +def update_function_docstrings(code: str, examples: dict[str, str]) -> str: + """Add or update the example section in selected function docstrings.""" + module = cst.parse_module(code) + return module.visit(DocstringTransformer(examples)).code + + +class DocstringTransformer(cst.CSTTransformer): + """Add, append, or replace example sections in function docstrings.""" + + def __init__(self, examples: dict[str, str]) -> None: + self.examples = examples + + def leave_FunctionDef( # noqa: N802 + self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef + ) -> cst.FunctionDef: + example = self.examples.get(original_node.name.value, "") + if not example: + return updated_node + + body = updated_node.body + if isinstance(body, cst.IndentedBlock): + statements = body.body + docstring = _get_docstring(statements) + if docstring is None: + statements = (_make_docstring(example), *statements) + else: + index, statement, value = docstring + new_value = _update_docstring(value.raw_value, example) + replacement = statement.with_changes( + body=(cst.Expr(value=cst.SimpleString(new_value)),) + ) + statements = (*statements[:index], replacement, *statements[index + 1 :]) + + return updated_node.with_changes(body=body.with_changes(body=statements)) + + statements = tuple(cst.SimpleStatementLine(body=(statement,)) for statement in body.body) + return updated_node.with_changes( + body=cst.IndentedBlock(body=(_make_docstring(example), *statements), indent=" ") + ) + + +def _get_docstring( + statements: tuple[cst.BaseStatement, ...], +) -> tuple[int, cst.SimpleStatementLine, cst.SimpleString] | None: + if not statements or not isinstance(statements[0], cst.SimpleStatementLine): + return None + + statement = statements[0] + if len(statement.body) != 1 or not isinstance(statement.body[0], cst.Expr): + return None + + value = statement.body[0].value + if not isinstance(value, cst.SimpleString): + return None + + return 0, statement, value + + +def _make_docstring(example: str) -> cst.SimpleStatementLine: + value = _update_docstring("", example) + return cst.SimpleStatementLine(body=(cst.Expr(value=cst.SimpleString(value)),)) + + +def _update_docstring(docstring: str, example: str) -> str: + example = _format_example(example) + pattern = ( + r"(?ms)^[ \t]*Papertrail examples:\n.*?\n[ \t]*::" + r"|^[ \t]*\.\. code-block:: python\n.*?\n[ \t]*::" + ) + matches = list(re.finditer(pattern, docstring)) + if matches: + first = matches[0] + sections = [docstring[: first.start()], example] + end = first.end() + for match in matches[1:]: + sections.append(docstring[end : match.start()]) + end = match.end() + sections.append(docstring[end:]) + new_doc = "".join(sections) + else: + new_doc = f"{docstring}\n\n{example}" + + return f'"""{new_doc.strip()}\n """' + + +def _format_example(example: str) -> str: + lines = example.splitlines(keepends=True) + if not lines: + return "" + + return textwrap.indent("".join(lines), " ", predicate=lambda line: bool(line.strip())) diff --git a/dev_tools/create_examples/transformation/format_examples.py b/dev_tools/create_examples/transformation/format_examples.py new file mode 100644 index 0000000..d8e479f --- /dev/null +++ b/dev_tools/create_examples/transformation/format_examples.py @@ -0,0 +1,50 @@ +from collections import defaultdict + +import black + +from ..collection.record import ExampleRecord # noqa: TID252 + + +def collect_example_strs(examples: list[dict]) -> dict[str, dict[str, list[str]]]: + fn_examples = defaultdict(_inner) + + for data in examples: + ex = ExampleRecord(**data) + + if ex.returned != ex.expected: + continue + + fn_examples[ex.src_file][ex.fn_name].append(example_to_str(ex)) + + return {k: dict(v) for k, v in fn_examples.items()} + + +def _inner() -> defaultdict: + return defaultdict(list) + + +def example_to_str(example: ExampleRecord) -> str: + sig = ", ".join( + part + for part in ( + ", ".join(map(str, example.args)), + ", ".join(f"{k}={v}" for k, v in example.kwargs.items()), + ) + if part + ) + expr = f"{example.ent_repr}({sig}) == {example.returned}" + formatted = black.format_str(expr, mode=black.Mode()) + lines = formatted.rstrip().splitlines() + doctest = "\n".join( + f"{' >>>' if i == 0 else ' ...'} {line}" for i, line in enumerate(lines) + ) + return f"{example.description or ''}\n\n.. code-block:: python\n\n{doctest}\n True" + + +def reduce_examples_to_example_str( + fn_examples: dict[str, dict[str, list[str]]], +) -> dict[str, dict[str, str]]: + return { + path: {k: "Papertrail examples:\n\n" + "\n".join(v) + "\n::" for k, v in fn.items()} + for path, fn in fn_examples.items() + } diff --git a/dev_tools/create_examples/transformation/transform.py b/dev_tools/create_examples/transformation/transform.py new file mode 100644 index 0000000..4850ef5 --- /dev/null +++ b/dev_tools/create_examples/transformation/transform.py @@ -0,0 +1,16 @@ +import json +from pathlib import Path + +from .ast_editing import update_function_docstrings +from .format_examples import collect_example_strs, reduce_examples_to_example_str + + +def update_modified_docstrings(examples_cache_path: Path) -> None: + examples = json.loads(examples_cache_path.read_text()) + reduced_examples = reduce_examples_to_example_str(collect_example_strs(examples)) + + for path, module_examples in reduced_examples.items(): + code = Path(path).read_text() + new_code = update_function_docstrings(code, module_examples) + if new_code != code: + Path(path).write_text(new_code) diff --git a/pyproject.toml b/pyproject.toml index 281bfd9..3505a3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ danom = ["py.typed"] dev = [ "hypothesis>=6.148.2", "ipykernel>=7.1.0", + "libcst>=1.9.0", "papertrail>=0.1.2", "pre-commit>=4.5.0", "pytest>=9.0.1", diff --git a/src/danom/_monads/__init__.py b/src/danom/_monads/__init__.py index a01afd8..4fb9694 100644 --- a/src/danom/_monads/__init__.py +++ b/src/danom/_monads/__init__.py @@ -1,6 +1,6 @@ from ._either import Either, Left, Right from ._option import Null, Option, Some -from ._result import Err, Ok, Result +from ._result_v2 import Err, Ok, Result from ._safe import safe, safe_method __all__ = [ diff --git a/src/danom/_monads/_either.py b/src/danom/_monads/_either.py index e6381c6..e8d6572 100644 --- a/src/danom/_monads/_either.py +++ b/src/danom/_monads/_either.py @@ -31,21 +31,7 @@ class Either[T_co, E_co: object](ABC): @classmethod def unit(cls, inner: T_co) -> Right[T_co]: - """Unit method. Given an item of type ``T`` return ``Right(T)`` - - .. doctest:: - - >>> from danom import Left, Right, Either - - >>> Either.unit(0) == Right(inner=0) - True - - >>> Right.unit(0) == Right(inner=0) - True - - >>> Left.unit(0) == Right(inner=0) - True - """ + """Unit method. Given an item of type ``T`` return ``Right(T)``""" return Right(inner) @abstractmethod @@ -53,15 +39,22 @@ def is_ok(self) -> bool: """Returns ``True`` if the result type is ``Right``. Returns ``False`` if the result type is ``Left``. - .. doctest:: - >>> from danom import Left, Right + Papertrail examples: + - >>> Right().is_ok() == True + + .. code-block:: python + + >>> Right(inner=None).is_ok() == True True - >>> Left().is_ok() == False + + .. code-block:: python + + >>> Left(inner=None).is_ok() == False True + :: """ ... @@ -70,12 +63,22 @@ def map(self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Either[U_co, """Pipe a pure function and wrap the return value with ``Right``. Given an ``Left`` will return ``self``. + + Papertrail examples: + + + .. code-block:: python - from danom import Left, Right + >>> Right(inner=0).map(add_one) == Right(inner=1) + True + + + .. code-block:: python - Right(1).map(add_one) == Right(2) - Left(1).map(add_one) == Left(1) + >>> Left(inner=None).map(add_one) == Left(inner=None) + True + :: """ ... @@ -84,106 +87,82 @@ def map_err(self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Either[U """Pipe a pure function and wrap the return value with ``Left``. Given an ``Right`` will return ``self``. - .. code-block:: python - from danom import Left, Right + Papertrail examples: - Left(TypeError()).map_err(type_err_to_value_err) == Left(ValueError()) - Right(1).map(type_err_to_value_err) == Right(1) - """ - ... - @abstractmethod - def and_then(self, func: Bindable, *args: P.args, **kwargs: P.kwargs) -> Either[U_co, E_co]: - """Pipe another function that returns a monad. For ``Left`` will return original inner. .. code-block:: python - from danom import Left, Right + >>> Right(inner=0).map_err(add_one) == Right(inner=0) + True + + + .. code-block:: python - Right(1).and_then(add_one) == Right(2) - Right(1).and_then(raise_err) == Left(TypeError()) - Left(TypeError()).and_then(add_one) == Left(TypeError()) - Left(TypeError()).and_then(raise_value_err) == Left(TypeError()) + >>> Left(inner=0).map_err(add_one) == Left(inner=1) + True + :: """ ... @abstractmethod - def or_else(self, func: Bindable, *args: P.args, **kwargs: P.kwargs) -> Either[U_co, E_co]: - """Pipe a function that returns a monad to recover from an ``Left``. For ``Right`` will return original ``Either``. - - .. code-block:: python - - from danom import Left, Right + def and_then(self, func: Bindable, *args: P.args, **kwargs: P.kwargs) -> Either[U_co, E_co]: + """Pipe another function that returns a monad. For ``Left`` will return original inner.""" + ... - Right(1).or_else(replace_err_with_zero) == Right(1) - Left(TypeError()).or_else(replace_err_with_zero) == Right(0) - """ + @abstractmethod + def or_else(self, func: Bindable, *args: P.args, **kwargs: P.kwargs) -> Either[U_co, E_co]: + """Pipe a function that returns a monad to recover from an ``Left``. For ``Right`` will return original ``Either``.""" ... @abstractmethod def unwrap(self) -> T_co: - """Unwrap the `Right` or ``Left`` monad to get the inner value. + """Unwrap the `Right` or ``Left`` monad to get the inner value.""" + ... - .. doctest:: + @staticmethod + def either_is_ok(result: Either[T_co, E_co]) -> bool: + """Check whether the monad is ok. Allows for ``filter`` or ``partition`` in a ``Stream`` without needing a lambda or custom function.""" + return result.is_ok() - >>> from danom import Left, Right + @staticmethod + def either_unwrap(result: Either[T_co, E_co]) -> T_co: + """Unwrap the `Right` or ``Left`` monad to get the inner value.""" + return result.unwrap() - >>> Right().unwrap() == None - True + def flatten(self) -> Either[T_co, E_co]: + """Flatten the monad. Will return the first ``Left`` or the lowest ``Right`` instance. - >>> Right(1).unwrap() == 1 - True - >>> Right("ok").unwrap() == 'ok' - True - >>> Left(-1).unwrap() == -1 - True + Papertrail examples: - """ - ... - @staticmethod - def either_is_ok(result: Either[T_co, E_co]) -> bool: - """Check whether the monad is ok. Allows for ``filter`` or ``partition`` in a ``Stream`` without needing a lambda or custom function. .. code-block:: python - from danom import Stream, Either - - Stream.from_iterable([Right(), Right(), Left()]).filter(Either.either_is_ok).collect() == (Right(), Right()) + >>> Right(inner=Right(inner=None)).flatten() == Right(inner=None) + True - """ - return result.is_ok() - - @staticmethod - def either_unwrap(result: Either[T_co, E_co]) -> T_co: - """Unwrap the `Right` or ``Left`` monad to get the inner value. .. code-block:: python - from danom import Stream, Either - - oks, errs = Stream.from_iterable([Right(1), Right(2), Left()]).partition(Either.either_is_ok) - oks.map(Either.either_unwrap).collect == (1, 2) + >>> Right(inner=Left(inner=None)).flatten() == Left(inner=None) + True - """ - return result.unwrap() - def flatten(self) -> Either[T_co, E_co]: - """Flatten the monad. Will return the first ``Left`` or the lowest ``Right`` instance. + .. code-block:: python - .. doctest:: + >>> Left(inner=Right(inner=None)).flatten() == Right(inner=None) + True - >>> from danom import Left, Right, Stream, Either - >>> Right(Right(Right(1))).flatten() == Right(1) - True + .. code-block:: python - >>> Right(Right(Left())).flatten() == Left() + >>> Left(inner=Left(inner=None)).flatten() == Left(inner=None) True - + :: """ current = self @@ -196,12 +175,60 @@ def flatten(self) -> Either[T_co, E_co]: @attrs.define(frozen=True, hash=True) class Right(Either[T_co, Never]): def is_ok(self) -> Literal[True]: + """Papertrail examples: + + + + .. code-block:: python + + >>> Right(inner=None).is_ok() == True + True + + + .. code-block:: python + + >>> Left(inner=None).is_ok() == False + True + :: + """ return True def map(self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Right[U_co]: + """Papertrail examples: + + + + .. code-block:: python + + >>> Right(inner=0).map(add_one) == Right(inner=1) + True + + + .. code-block:: python + + >>> Left(inner=None).map(add_one) == Left(inner=None) + True + :: + """ return Right(func(self.inner, *args, **kwargs)) def map_err(self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Self: # noqa: ARG002 + """Papertrail examples: + + + + .. code-block:: python + + >>> Right(inner=0).map_err(add_one) == Right(inner=0) + True + + + .. code-block:: python + + >>> Left(inner=0).map_err(add_one) == Left(inner=1) + True + :: + """ return self def and_then(self, func: Bindable, *args: P.args, **kwargs: P.kwargs) -> Either[U_co, E_co]: @@ -217,12 +244,60 @@ def unwrap(self) -> T_co: @attrs.define(frozen=True, hash=True) class Left(Either[Never, E_co]): def is_ok(self) -> Literal[False]: + """Papertrail examples: + + + + .. code-block:: python + + >>> Right(inner=None).is_ok() == True + True + + + .. code-block:: python + + >>> Left(inner=None).is_ok() == False + True + :: + """ return False def map(self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Self: # noqa: ARG002 + """Papertrail examples: + + + + .. code-block:: python + + >>> Right(inner=0).map(add_one) == Right(inner=1) + True + + + .. code-block:: python + + >>> Left(inner=None).map(add_one) == Left(inner=None) + True + :: + """ return self def map_err(self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Left[F_co]: + """Papertrail examples: + + + + .. code-block:: python + + >>> Right(inner=0).map_err(add_one) == Right(inner=0) + True + + + .. code-block:: python + + >>> Left(inner=0).map_err(add_one) == Left(inner=1) + True + :: + """ return Left(func(self.inner, *args, **kwargs)) def and_then(self, func: Bindable, *args: P.args, **kwargs: P.kwargs) -> Self: # noqa: ARG002 diff --git a/src/danom/_monads/_option.py b/src/danom/_monads/_option.py index 55297c0..0c7c23e 100644 --- a/src/danom/_monads/_option.py +++ b/src/danom/_monads/_option.py @@ -3,95 +3,626 @@ from abc import ABC, abstractmethod from collections.abc import Callable from copy import deepcopy -from typing import cast +from typing import TYPE_CHECKING, cast import attrs -from ._result import Err, Ok, Result +if TYPE_CHECKING: + from ._result_v2 import Result @attrs.define(frozen=True) class Option[T](ABC): @abstractmethod - def and_(self, opt_b: Option[T]) -> Option[T]: ... + def and_(self, opt_b: Option[T]) -> Option[T]: + """Papertrail examples: + + A ``Some`` combined with ``Null`` through ``and_`` produces ``Null``. + + .. code-block:: python + + >>> Some(inner=2).and_(Null()) == Null() + True + A ``Null`` combined with an ``Some`` through ``and_`` remains ``Null``. + + .. code-block:: python + + >>> Null().and_(Some(inner="foo")) == Null() + True + When both options contain values, ``and_`` keeps the second ``Some``. + + .. code-block:: python + + >>> Some(inner=2).and_(Some(inner="foo")) == Some(inner="foo") + True + Combining two ``Null`` values with ``and_`` produces ``Null``. + + .. code-block:: python + + >>> Null().and_(Null()) == Null() + True + :: + """ + ... @abstractmethod - def and_then[U](self, fn: Callable[[T], Option[U]]) -> Option[T]: ... + def and_then[U](self, fn: Callable[[T], Option[U]]) -> Option[T]: + """Papertrail examples: + + A successful function passed to ``and_then`` returns its ``Some`` result. + + .. code-block:: python + + >>> Some(inner=2).and_then(must_be_less_than_10) == Some(inner=2) + True + When the function returns ``Null``, ``and_then`` passes that ``Null`` through. + + .. code-block:: python + + >>> Some(inner=20).and_then(must_be_less_than_10) == Null() + True + An existing ``Null`` skips the function passed to ``and_then``. + + .. code-block:: python + + >>> Null().and_then(must_be_less_than_10) == Null() + True + :: + """ + ... @abstractmethod - def as_list(self) -> list[T]: ... + def as_list(self) -> list[T]: + """Papertrail examples: + + A ``Some`` becomes a one-item list through ``as_list``. + + .. code-block:: python + + >>> Some(inner=2).as_list() == [2] + True + ``as_list`` represents ``Null`` as an empty list. + + .. code-block:: python + + >>> Null().as_list() == [] + True + :: + """ + ... @abstractmethod - def as_tuple(self) -> tuple[T, ...]: ... + def as_tuple(self) -> tuple[T, ...]: + """Papertrail examples: + + A ``Some`` becomes a one-item tuple through ``as_tuple``. + + .. code-block:: python + + >>> Some(inner=2).as_tuple() == (2,) + True + ``as_tuple`` represents ``Null`` as an empty tuple. + + .. code-block:: python + + >>> Null().as_tuple() == () + True + :: + """ + ... def cloned(self) -> Option[T]: + """Papertrail examples: + + Cloning a ``Some`` creates a separate option with the same value. + + .. code-block:: python + + >>> Some(inner=2).cloned() == Some(inner=2) + True + Cloning ``Null`` creates a separate ``Null``. + + .. code-block:: python + + >>> Null().cloned() == Null() + True + :: + """ return deepcopy(self) @abstractmethod - def expect(self, msg: str) -> T: ... + def expect(self, msg: str) -> T: + """Papertrail examples: + + ``expect`` extracts the value from a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).expect("must be positive") == 2 + True + :: + """ + ... @abstractmethod - def filter_(self, predicate: Callable[[T], bool]) -> Option[T]: ... + def filter_(self, predicate: Callable[[T], bool]) -> Option[T]: + """Papertrail examples: + + A ``Some`` that fails the predicate becomes ``Null`` through ``filter_``. + + .. code-block:: python + + >>> Some(inner=3).filter_(is_even) == Null() + True + A ``Some`` that passes the predicate remains unchanged. + + .. code-block:: python + + >>> Some(inner=4).filter_(is_even) == Some(inner=4) + True + ``filter_`` leaves ``Null`` unchanged without calling the predicate. + + .. code-block:: python + + >>> Null().filter_(is_even) == Null() + True + :: + """ + ... @abstractmethod - def flatten(self) -> Option[T]: ... + def flatten(self) -> Option[T]: + """Papertrail examples: + + Flattening three nested ``Some`` values removes only the outer option. + + .. code-block:: python + + >>> Some(inner=Some(inner=Some(inner=2))).flatten() == Some(inner=Some(inner=2)) + True + Flattening a doubly wrapped value returns the inner ``Some``. + + .. code-block:: python + + >>> Some(inner=Some(inner=2)).flatten() == Some(inner=2) + True + Flattening a ``Some`` with a non-option value does nothing. + + .. code-block:: python + + >>> Some(inner=2).flatten() == Some(inner=2) + True + A ``Null`` passes through ``flatten`` unchanged. + + .. code-block:: python + + >>> Null().flatten() == Null() + True + :: + """ + ... @abstractmethod - def inspect(self, fn: Callable[[T], None]) -> Option[T]: ... + def inspect(self, fn: Callable[[T], None]) -> Option[T]: + """Papertrail examples: + + Inspecting a ``Some`` calls the function and returns the original option. + + .. code-block:: python + + >>> Some(inner=[1]).inspect(append_to_list) == Some(inner=[1]) + True + Inspecting ``Null`` returns ``Null`` without calling the function. + + .. code-block:: python + + >>> Null().inspect(append_to_list) == Null() + True + :: + """ + ... @abstractmethod - def is_none(self) -> bool: ... + def is_none(self) -> bool: + """Papertrail examples: + + ``is_none`` reports ``False`` for a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).is_none() == False + True + For ``Null``, ``is_none`` reports ``True``. + + .. code-block:: python + + >>> Null().is_none() == True + True + :: + """ + ... @abstractmethod - def is_none_or(self, fn: Callable[[T], bool]) -> bool: ... + def is_none_or(self, fn: Callable[[T], bool]) -> bool: + """Papertrail examples: + + When the predicate rejects a ``Some``, ``is_none_or`` reports ``False``. + + .. code-block:: python + + >>> Some(inner=1).is_none_or(is_even) == False + True + When the predicate accepts a ``Some``, ``is_none_or`` reports ``True``. + + .. code-block:: python + + >>> Some(inner=2).is_none_or(is_even) == True + True + ``Null`` makes ``is_none_or`` report ``True`` without calling the predicate. + + .. code-block:: python + + >>> Null().is_none_or(is_even) == True + True + :: + """ + ... @abstractmethod - def is_some(self) -> bool: ... + def is_some(self) -> bool: + """Papertrail examples: + + ``is_some`` reports ``True`` for a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).is_some() == True + True + For ``Null``, ``is_some`` reports ``False``. + + .. code-block:: python + + >>> Null().is_some() == False + True + :: + """ + ... @abstractmethod - def is_some_and(self, fn: Callable[[T], bool]) -> bool: ... + def is_some_and(self, fn: Callable[[T], bool]) -> bool: + """Papertrail examples: + + When the predicate rejects a ``Some``, ``is_some_and`` reports ``False``. + + .. code-block:: python + + >>> Some(inner=1).is_some_and(is_even) == False + True + When the predicate accepts a ``Some``, ``is_some_and`` reports ``True``. + + .. code-block:: python + + >>> Some(inner=2).is_some_and(is_even) == True + True + A ``Null`` makes ``is_some_and`` report ``False`` without calling the predicate. + + .. code-block:: python + + >>> Null().is_some_and(is_even) == False + True + :: + """ + ... @abstractmethod - def map[U](self, fn: Callable[[T], U]) -> Option[U]: ... + def map[U](self, fn: Callable[[T], U]) -> Option[U]: + """Papertrail examples: + + Mapping a ``Some`` applies the function and wraps its result in a new ``Some``. + + .. code-block:: python + + >>> Some(inner=1).map(add_one) == Some(inner=2) + True + Mapping ``Null`` leaves it unchanged and skips the function. + + .. code-block:: python + + >>> Null().map(add_one) == Null() + True + :: + """ + ... @abstractmethod - def map_or[U](self, default: U, fn: Callable[[T], U]) -> U: ... + def map_or[U](self, default: U, fn: Callable[[T], U]) -> U: + """Papertrail examples: + + For a ``Some``, ``map_or`` uses the function result instead of the default. + + .. code-block:: python + + >>> Some(inner="foo").map_or(42, len) == 3 + True + For ``Null``, ``map_or`` returns the supplied default. + + .. code-block:: python + + >>> Null().map_or(42, len) == 42 + True + :: + """ + ... @abstractmethod - def map_or_else[U](self, default: Callable[..., U], fn: Callable[[T], U]) -> U: ... + def map_or_else[U](self, default: Callable[..., U], fn: Callable[[T], U]) -> U: + """Papertrail examples: + + A ``Some`` makes ``map_or_else`` use the mapping function. + + .. code-block:: python + + >>> Some(inner="foo").map_or_else(get_42, len) == 3 + True + A ``Null`` makes ``map_or_else`` use the default function. + + .. code-block:: python + + >>> Null().map_or_else(get_42, len) == 42 + True + :: + """ + ... @abstractmethod - def ok_or[E](self, err: E) -> Result[T, E]: ... + def ok_or[E](self, err: E) -> Result[T, E]: + """Papertrail examples: + + A ``Some`` converts to ``Ok`` through ``ok_or``. + + .. code-block:: python + + >>> Some(inner="foo").ok_or(0) == Ok(inner="foo") + True + A ``Null`` converts to ``Err`` through ``ok_or``. + + .. code-block:: python + + >>> Null().ok_or(0) == Err(error=0) + True + :: + """ + ... @abstractmethod - def ok_or_else[E](self, err: Callable[..., E]) -> Result[T, E]: ... + def ok_or_else[E](self, err: Callable[..., E]) -> Result[T, E]: + """Papertrail examples: + + A ``Some`` converts to ``Ok`` without calling the error function. + + .. code-block:: python + + >>> Some(inner="foo").ok_or_else(get_42) == Ok(inner="foo") + True + A ``Null`` converts to ``Err`` using the error function result. + + .. code-block:: python + + >>> Null().ok_or_else(get_42) == Err(error=42) + True + :: + """ + ... @abstractmethod - def or_(self, opt_b: Option[T]) -> Option[T]: ... + def or_(self, opt_b: Option[T]) -> Option[T]: + """Papertrail examples: + + A ``Some`` keeps its value when ``or_`` receives ``Null``. + + .. code-block:: python + + >>> Some(inner=2).or_(Null()) == Some(inner=2) + True + A ``Null`` gives way to a ``Some`` passed to ``or_``. + + .. code-block:: python + + >>> Null().or_(Some(inner=100)) == Some(inner=100) + True + When both options contain values, ``or_`` keeps the first ``Some``. + + .. code-block:: python + + >>> Some(inner=2).or_(Some(inner=100)) == Some(inner=2) + True + When both options are ``Null``, ``or_`` returns ``Null``. + + .. code-block:: python + + >>> Null().or_(Null()) == Null() + True + :: + """ + ... @abstractmethod - def or_else(self, opt_b: Callable[..., Option[T]]) -> Option[T]: ... + def or_else(self, opt_b: Callable[..., Option[T]]) -> Option[T]: + """Papertrail examples: + + A ``Some`` passes through ``or_else`` without calling the function. + + .. code-block:: python + + >>> Some(inner="barbarians").or_else(get_some_vikings) == Some(inner="barbarians") + True + For ``Null``, ``or_else`` returns the ``Some`` produced by the function. + + .. code-block:: python + + >>> Null().or_else(get_some_vikings) == Some(inner="vikings") + True + If the fallback also produces ``Null``, ``or_else`` returns ``Null``. + + .. code-block:: python + + >>> Null().or_else(Null) == Null() + True + :: + """ + ... @abstractmethod - def replace(self, value: T) -> Option[T]: ... + def replace(self, value: T) -> Option[T]: + """Papertrail examples: + + Replacing a ``Some`` returns a new ``Some`` with the replacement value. + + .. code-block:: python + + >>> Some(inner=2).replace(5) == Some(inner=5) + True + Replacing ``Null`` leaves it as ``Null``. + + .. code-block:: python + + >>> Null().replace(3) == Null() + True + :: + """ + ... @abstractmethod - def transpose[E](self) -> Result[Option[T], E]: ... + def transpose[E](self) -> Result[Option[T], E]: + """Papertrail examples: + + Transposing ``Some(Ok(value))`` produces ``Ok(Some(value))``. + + .. code-block:: python + + >>> Some(inner=Ok(inner=2)).transpose() == Ok(inner=Some(inner=2)) + True + Transposing ``Some(Err(error))`` produces ``Err(Some(error))``. + + .. code-block:: python + + >>> Some(inner=Err(error=2)).transpose() == Err(error=Some(inner=2)) + True + Transposing ``Null`` produces ``Ok(Null())``. + + .. code-block:: python + + >>> Null().transpose() == Ok(inner=Null()) + True + :: + """ + ... @abstractmethod - def unwrap(self) -> T: ... + def unwrap(self) -> T: + """Papertrail examples: + + ``unwrap`` extracts the value from a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).unwrap() == 2 + True + :: + """ + ... @abstractmethod - def unwrap_or(self, default: T) -> T: ... + def unwrap_or(self, default: T) -> T: + """Papertrail examples: + + With a ``Some``, ``unwrap_or`` returns the value and ignores the default. + + .. code-block:: python + + >>> Some(inner="car").unwrap_or("bike") == "car" + True + With ``Null``, ``unwrap_or`` returns the default. + + .. code-block:: python + + >>> Null().unwrap_or("bike") == "bike" + True + :: + """ + ... @abstractmethod - def unwrap_or_else(self, fn: Callable[..., T]) -> T: ... + def unwrap_or_else(self, fn: Callable[..., T]) -> T: + """Papertrail examples: + + A ``Some`` makes ``unwrap_or_else`` return its value without calling the function. + + .. code-block:: python + + >>> Some(inner=4).unwrap_or_else(get_42) == 4 + True + A ``Null`` makes ``unwrap_or_else`` return the function result. + + .. code-block:: python + + >>> Null().unwrap_or_else(get_42) == 42 + True + :: + """ + ... @abstractmethod - def zip[U](self, other: Option[U]) -> Option[tuple[T, U]]: ... + def zip[U](self, other: Option[U]) -> Option[tuple[T, U]]: + """Papertrail examples: + + Zipping two ``Some`` values produces a ``Some`` containing both values. + + .. code-block:: python + + >>> Some(inner=1).zip(Some(inner="hi")) == Some(inner=(1, "hi")) + True + Zipping a ``Some`` with ``Null`` produces ``Null``. + + .. code-block:: python + + >>> Some(inner=1).zip(Null()) == Null() + True + Zipping ``Null`` with a ``Some`` produces ``Null``. + + .. code-block:: python + + >>> Null().zip(Some(inner=1)) == Null() + True + :: + """ + ... @abstractmethod - def unzip[U](self) -> tuple[Option[T], Option[U]]: ... + def unzip[U](self) -> tuple[Option[T], Option[U]]: + """Papertrail examples: + + Unzipping a ``Some`` pair produces two ``Some`` values. + + .. code-block:: python + + >>> Some(inner=(2, 2)).unzip() == (Some(inner=2), Some(inner=2)) + True + Unzipping a ``Some`` with a non-pair value produces two ``Null`` values. + + .. code-block:: python + + >>> Some(inner=4).unzip() == (Null(), Null()) + True + Unzipping ``Null`` produces two ``Null`` values. + + .. code-block:: python + + >>> Null().unzip() == (Null(), Null()) + True + :: + """ + ... @attrs.define(frozen=True) @@ -99,90 +630,584 @@ class Some[T](Option): inner: T def and_(self, opt_b: Option[T]) -> Option[T]: + """Papertrail examples: + + A ``Some`` combined with ``Null`` through ``and_`` produces ``Null``. + + .. code-block:: python + + >>> Some(inner=2).and_(Null()) == Null() + True + A ``Null`` combined with an ``Some`` through ``and_`` remains ``Null``. + + .. code-block:: python + + >>> Null().and_(Some(inner="foo")) == Null() + True + When both options contain values, ``and_`` keeps the second ``Some``. + + .. code-block:: python + + >>> Some(inner=2).and_(Some(inner="foo")) == Some(inner="foo") + True + Combining two ``Null`` values with ``and_`` produces ``Null``. + + .. code-block:: python + + >>> Null().and_(Null()) == Null() + True + :: + """ return opt_b def and_then[U](self, fn: Callable[[T], Option[U]]) -> Option[U]: + """Papertrail examples: + + A successful function passed to ``and_then`` returns its ``Some`` result. + + .. code-block:: python + + >>> Some(inner=2).and_then(must_be_less_than_10) == Some(inner=2) + True + When the function returns ``Null``, ``and_then`` passes that ``Null`` through. + + .. code-block:: python + + >>> Some(inner=20).and_then(must_be_less_than_10) == Null() + True + An existing ``Null`` skips the function passed to ``and_then``. + + .. code-block:: python + + >>> Null().and_then(must_be_less_than_10) == Null() + True + :: + """ return fn(self.inner) def as_list(self) -> list[T]: + """Papertrail examples: + + A ``Some`` becomes a one-item list through ``as_list``. + + .. code-block:: python + + >>> Some(inner=2).as_list() == [2] + True + ``as_list`` represents ``Null`` as an empty list. + + .. code-block:: python + + >>> Null().as_list() == [] + True + :: + """ return [self.inner] def as_tuple(self) -> tuple[T, ...]: + """Papertrail examples: + + A ``Some`` becomes a one-item tuple through ``as_tuple``. + + .. code-block:: python + + >>> Some(inner=2).as_tuple() == (2,) + True + ``as_tuple`` represents ``Null`` as an empty tuple. + + .. code-block:: python + + >>> Null().as_tuple() == () + True + :: + """ return (self.inner,) def expect(self, msg: str) -> T: # noqa: ARG002 + """Papertrail examples: + + ``expect`` extracts the value from a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).expect("must be positive") == 2 + True + :: + """ return self.inner def filter_(self, predicate: Callable[[T], bool]) -> Option[T]: + """Papertrail examples: + + A ``Some`` that fails the predicate becomes ``Null`` through ``filter_``. + + .. code-block:: python + + >>> Some(inner=3).filter_(is_even) == Null() + True + A ``Some`` that passes the predicate remains unchanged. + + .. code-block:: python + + >>> Some(inner=4).filter_(is_even) == Some(inner=4) + True + ``filter_`` leaves ``Null`` unchanged without calling the predicate. + + .. code-block:: python + + >>> Null().filter_(is_even) == Null() + True + :: + """ return self if predicate(self.inner) else Null() def flatten(self) -> Option[T]: + """Papertrail examples: + + Flattening three nested ``Some`` values removes only the outer option. + + .. code-block:: python + + >>> Some(inner=Some(inner=Some(inner=2))).flatten() == Some(inner=Some(inner=2)) + True + Flattening a doubly wrapped value returns the inner ``Some``. + + .. code-block:: python + + >>> Some(inner=Some(inner=2)).flatten() == Some(inner=2) + True + Flattening a ``Some`` with a non-option value does nothing. + + .. code-block:: python + + >>> Some(inner=2).flatten() == Some(inner=2) + True + A ``Null`` passes through ``flatten`` unchanged. + + .. code-block:: python + + >>> Null().flatten() == Null() + True + :: + """ if isinstance(self.inner, Some): return self.inner return self def inspect(self, fn: Callable[[T], None]) -> Option[T]: + """Papertrail examples: + + Inspecting a ``Some`` calls the function and returns the original option. + + .. code-block:: python + + >>> Some(inner=[1]).inspect(append_to_list) == Some(inner=[1]) + True + Inspecting ``Null`` returns ``Null`` without calling the function. + + .. code-block:: python + + >>> Null().inspect(append_to_list) == Null() + True + :: + """ fn(deepcopy(self.inner)) return self def is_none(self) -> bool: + """Papertrail examples: + + ``is_none`` reports ``False`` for a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).is_none() == False + True + For ``Null``, ``is_none`` reports ``True``. + + .. code-block:: python + + >>> Null().is_none() == True + True + :: + """ return False def is_none_or(self, fn: Callable[[T], bool]) -> bool: + """Papertrail examples: + + When the predicate rejects a ``Some``, ``is_none_or`` reports ``False``. + + .. code-block:: python + + >>> Some(inner=1).is_none_or(is_even) == False + True + When the predicate accepts a ``Some``, ``is_none_or`` reports ``True``. + + .. code-block:: python + + >>> Some(inner=2).is_none_or(is_even) == True + True + ``Null`` makes ``is_none_or`` report ``True`` without calling the predicate. + + .. code-block:: python + + >>> Null().is_none_or(is_even) == True + True + :: + """ return fn(self.inner) def is_some(self) -> bool: + """Papertrail examples: + + ``is_some`` reports ``True`` for a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).is_some() == True + True + For ``Null``, ``is_some`` reports ``False``. + + .. code-block:: python + + >>> Null().is_some() == False + True + :: + """ return True def is_some_and(self, fn: Callable[[T], bool]) -> bool: + """Papertrail examples: + + When the predicate rejects a ``Some``, ``is_some_and`` reports ``False``. + + .. code-block:: python + + >>> Some(inner=1).is_some_and(is_even) == False + True + When the predicate accepts a ``Some``, ``is_some_and`` reports ``True``. + + .. code-block:: python + + >>> Some(inner=2).is_some_and(is_even) == True + True + A ``Null`` makes ``is_some_and`` report ``False`` without calling the predicate. + + .. code-block:: python + + >>> Null().is_some_and(is_even) == False + True + :: + """ return fn(self.inner) def map[U](self, fn: Callable[[T], U]) -> Option[U]: + """Papertrail examples: + + Mapping a ``Some`` applies the function and wraps its result in a new ``Some``. + + .. code-block:: python + + >>> Some(inner=1).map(add_one) == Some(inner=2) + True + Mapping ``Null`` leaves it unchanged and skips the function. + + .. code-block:: python + + >>> Null().map(add_one) == Null() + True + :: + """ return Some(fn(self.inner)) def map_or[U](self, default: U, fn: Callable[[T], U]) -> U: # noqa: ARG002 + """Papertrail examples: + + For a ``Some``, ``map_or`` uses the function result instead of the default. + + .. code-block:: python + + >>> Some(inner="foo").map_or(42, len) == 3 + True + For ``Null``, ``map_or`` returns the supplied default. + + .. code-block:: python + + >>> Null().map_or(42, len) == 42 + True + :: + """ return fn(self.inner) def map_or_else[U](self, default: Callable[[], U], fn: Callable[[T], U]) -> U: # noqa: ARG002 + """Papertrail examples: + + A ``Some`` makes ``map_or_else`` use the mapping function. + + .. code-block:: python + + >>> Some(inner="foo").map_or_else(get_42, len) == 3 + True + A ``Null`` makes ``map_or_else`` use the default function. + + .. code-block:: python + + >>> Null().map_or_else(get_42, len) == 42 + True + :: + """ return fn(self.inner) def ok_or[E](self, err: E) -> Result[T, E]: # noqa: ARG002 - return Ok(self.inner) + """Papertrail examples: + + A ``Some`` converts to ``Ok`` through ``ok_or``. + + .. code-block:: python + + >>> Some(inner="foo").ok_or(0) == Ok(inner="foo") + True + A ``Null`` converts to ``Err`` through ``ok_or``. + + .. code-block:: python + + >>> Null().ok_or(0) == Err(error=0) + True + :: + """ + from ._result_v2 import Ok, Result # noqa: PLC0415 + + return cast(Result[T, E], Ok(self.inner)) def ok_or_else[E](self, err: Callable[[], E]) -> Result[T, E]: # noqa: ARG002 - return Ok(self.inner) + """Papertrail examples: + + A ``Some`` converts to ``Ok`` without calling the error function. + + .. code-block:: python + + >>> Some(inner="foo").ok_or_else(get_42) == Ok(inner="foo") + True + A ``Null`` converts to ``Err`` using the error function result. + + .. code-block:: python + + >>> Null().ok_or_else(get_42) == Err(error=42) + True + :: + """ + from ._result_v2 import Ok, Result # noqa: PLC0415 + + return cast(Result[T, E], Ok(self.inner)) def or_(self, opt_b: Option[T]) -> Option[T]: # noqa: ARG002 + """Papertrail examples: + + A ``Some`` keeps its value when ``or_`` receives ``Null``. + + .. code-block:: python + + >>> Some(inner=2).or_(Null()) == Some(inner=2) + True + A ``Null`` gives way to a ``Some`` passed to ``or_``. + + .. code-block:: python + + >>> Null().or_(Some(inner=100)) == Some(inner=100) + True + When both options contain values, ``or_`` keeps the first ``Some``. + + .. code-block:: python + + >>> Some(inner=2).or_(Some(inner=100)) == Some(inner=2) + True + When both options are ``Null``, ``or_`` returns ``Null``. + + .. code-block:: python + + >>> Null().or_(Null()) == Null() + True + :: + """ return self def or_else(self, opt_b: Callable[[], Option[T]]) -> Option[T]: # noqa: ARG002 + """Papertrail examples: + + A ``Some`` passes through ``or_else`` without calling the function. + + .. code-block:: python + + >>> Some(inner="barbarians").or_else(get_some_vikings) == Some(inner="barbarians") + True + For ``Null``, ``or_else`` returns the ``Some`` produced by the function. + + .. code-block:: python + + >>> Null().or_else(get_some_vikings) == Some(inner="vikings") + True + If the fallback also produces ``Null``, ``or_else`` returns ``Null``. + + .. code-block:: python + + >>> Null().or_else(Null) == Null() + True + :: + """ return self def replace(self, value: T) -> Option[T]: + """Papertrail examples: + + Replacing a ``Some`` returns a new ``Some`` with the replacement value. + + .. code-block:: python + + >>> Some(inner=2).replace(5) == Some(inner=5) + True + Replacing ``Null`` leaves it as ``Null``. + + .. code-block:: python + + >>> Null().replace(3) == Null() + True + :: + """ return Some(value) def transpose(self) -> Result[Option[T], Option[T]]: + """Papertrail examples: + + Transposing ``Some(Ok(value))`` produces ``Ok(Some(value))``. + + .. code-block:: python + + >>> Some(inner=Ok(inner=2)).transpose() == Ok(inner=Some(inner=2)) + True + Transposing ``Some(Err(error))`` produces ``Err(Some(error))``. + + .. code-block:: python + + >>> Some(inner=Err(error=2)).transpose() == Err(error=Some(inner=2)) + True + Transposing ``Null`` produces ``Ok(Null())``. + + .. code-block:: python + + >>> Null().transpose() == Ok(inner=Null()) + True + :: + """ + from ._result_v2 import Err, Ok, Result # noqa: PLC0415 + if isinstance(self.inner, Ok): - return Ok(Some(self.inner.inner)) + return cast(Result[Option[T], Option[T]], Ok(Some(self.inner.inner))) if isinstance(self.inner, Err): return cast(Result[Option[T], Option[T]], Err[Option[T]](Some(self.inner.error))) raise TypeError("inner must be a `Result` type") def unwrap(self) -> T: + """Papertrail examples: + + ``unwrap`` extracts the value from a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).unwrap() == 2 + True + :: + """ return self.inner def unwrap_or(self, default: T) -> T: # noqa: ARG002 + """Papertrail examples: + + With a ``Some``, ``unwrap_or`` returns the value and ignores the default. + + .. code-block:: python + + >>> Some(inner="car").unwrap_or("bike") == "car" + True + With ``Null``, ``unwrap_or`` returns the default. + + .. code-block:: python + + >>> Null().unwrap_or("bike") == "bike" + True + :: + """ return self.inner def unwrap_or_else(self, fn: Callable[[], T]) -> T: # noqa: ARG002 + """Papertrail examples: + + A ``Some`` makes ``unwrap_or_else`` return its value without calling the function. + + .. code-block:: python + + >>> Some(inner=4).unwrap_or_else(get_42) == 4 + True + A ``Null`` makes ``unwrap_or_else`` return the function result. + + .. code-block:: python + + >>> Null().unwrap_or_else(get_42) == 42 + True + :: + """ return self.inner def zip[U](self, other: Option[U]) -> Option[tuple[T, U]]: + """Papertrail examples: + + Zipping two ``Some`` values produces a ``Some`` containing both values. + + .. code-block:: python + + >>> Some(inner=1).zip(Some(inner="hi")) == Some(inner=(1, "hi")) + True + Zipping a ``Some`` with ``Null`` produces ``Null``. + + .. code-block:: python + + >>> Some(inner=1).zip(Null()) == Null() + True + Zipping ``Null`` with a ``Some`` produces ``Null``. + + .. code-block:: python + + >>> Null().zip(Some(inner=1)) == Null() + True + :: + """ if isinstance(other, Some): return Some[tuple[T, U]]((self.inner, other.inner)) return Null[tuple[T, U]]() def unzip[U](self) -> tuple[Option[T], Option[U]]: + """Papertrail examples: + + Unzipping a ``Some`` pair produces two ``Some`` values. + + .. code-block:: python + + >>> Some(inner=(2, 2)).unzip() == (Some(inner=2), Some(inner=2)) + True + Unzipping a ``Some`` with a non-pair value produces two ``Null`` values. + + .. code-block:: python + + >>> Some(inner=4).unzip() == (Null(), Null()) + True + Unzipping ``Null`` produces two ``Null`` values. + + .. code-block:: python + + >>> Null().unzip() == (Null(), Null()) + True + :: + """ if isinstance(self.inner, tuple) and len(self.inner) == 2: # noqa: PLR2004 return (Some(self.inner[0]), Some(self.inner[1])) return (Null(), Null()) @@ -191,79 +1216,573 @@ def unzip[U](self) -> tuple[Option[T], Option[U]]: @attrs.define(frozen=True) class Null[T](Option): def and_(self, opt_b: Option[T]) -> Option[T]: # noqa: ARG002 + """Papertrail examples: + + A ``Some`` combined with ``Null`` through ``and_`` produces ``Null``. + + .. code-block:: python + + >>> Some(inner=2).and_(Null()) == Null() + True + A ``Null`` combined with an ``Some`` through ``and_`` remains ``Null``. + + .. code-block:: python + + >>> Null().and_(Some(inner="foo")) == Null() + True + When both options contain values, ``and_`` keeps the second ``Some``. + + .. code-block:: python + + >>> Some(inner=2).and_(Some(inner="foo")) == Some(inner="foo") + True + Combining two ``Null`` values with ``and_`` produces ``Null``. + + .. code-block:: python + + >>> Null().and_(Null()) == Null() + True + :: + """ return self def and_then[U](self, fn: Callable[[T], Option[U]]) -> Option[U]: # noqa: ARG002 + """Papertrail examples: + + A successful function passed to ``and_then`` returns its ``Some`` result. + + .. code-block:: python + + >>> Some(inner=2).and_then(must_be_less_than_10) == Some(inner=2) + True + When the function returns ``Null``, ``and_then`` passes that ``Null`` through. + + .. code-block:: python + + >>> Some(inner=20).and_then(must_be_less_than_10) == Null() + True + An existing ``Null`` skips the function passed to ``and_then``. + + .. code-block:: python + + >>> Null().and_then(must_be_less_than_10) == Null() + True + :: + """ return self def as_list(self) -> list[T]: + """Papertrail examples: + + A ``Some`` becomes a one-item list through ``as_list``. + + .. code-block:: python + + >>> Some(inner=2).as_list() == [2] + True + ``as_list`` represents ``Null`` as an empty list. + + .. code-block:: python + + >>> Null().as_list() == [] + True + :: + """ return [] def as_tuple(self) -> tuple[T, ...]: + """Papertrail examples: + + A ``Some`` becomes a one-item tuple through ``as_tuple``. + + .. code-block:: python + + >>> Some(inner=2).as_tuple() == (2,) + True + ``as_tuple`` represents ``Null`` as an empty tuple. + + .. code-block:: python + + >>> Null().as_tuple() == () + True + :: + """ return () def expect(self, msg: str) -> T: + """Papertrail examples: + + ``expect`` extracts the value from a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).expect("must be positive") == 2 + True + :: + """ raise ValueError(msg) def filter_(self, predicate: Callable[[T], bool]) -> Option[T]: # noqa: ARG002 + """Papertrail examples: + + A ``Some`` that fails the predicate becomes ``Null`` through ``filter_``. + + .. code-block:: python + + >>> Some(inner=3).filter_(is_even) == Null() + True + A ``Some`` that passes the predicate remains unchanged. + + .. code-block:: python + + >>> Some(inner=4).filter_(is_even) == Some(inner=4) + True + ``filter_`` leaves ``Null`` unchanged without calling the predicate. + + .. code-block:: python + + >>> Null().filter_(is_even) == Null() + True + :: + """ return self def flatten(self) -> Option[T]: + """Papertrail examples: + + Flattening three nested ``Some`` values removes only the outer option. + + .. code-block:: python + + >>> Some(inner=Some(inner=Some(inner=2))).flatten() == Some(inner=Some(inner=2)) + True + Flattening a doubly wrapped value returns the inner ``Some``. + + .. code-block:: python + + >>> Some(inner=Some(inner=2)).flatten() == Some(inner=2) + True + Flattening a ``Some`` with a non-option value does nothing. + + .. code-block:: python + + >>> Some(inner=2).flatten() == Some(inner=2) + True + A ``Null`` passes through ``flatten`` unchanged. + + .. code-block:: python + + >>> Null().flatten() == Null() + True + :: + """ return self def inspect(self, fn: Callable[[T], None]) -> Option[T]: # noqa: ARG002 + """Papertrail examples: + + Inspecting a ``Some`` calls the function and returns the original option. + + .. code-block:: python + + >>> Some(inner=[1]).inspect(append_to_list) == Some(inner=[1]) + True + Inspecting ``Null`` returns ``Null`` without calling the function. + + .. code-block:: python + + >>> Null().inspect(append_to_list) == Null() + True + :: + """ return self def is_none(self) -> bool: + """Papertrail examples: + + ``is_none`` reports ``False`` for a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).is_none() == False + True + For ``Null``, ``is_none`` reports ``True``. + + .. code-block:: python + + >>> Null().is_none() == True + True + :: + """ return True def is_none_or(self, fn: Callable[[T], bool]) -> bool: # noqa: ARG002 + """Papertrail examples: + + When the predicate rejects a ``Some``, ``is_none_or`` reports ``False``. + + .. code-block:: python + + >>> Some(inner=1).is_none_or(is_even) == False + True + When the predicate accepts a ``Some``, ``is_none_or`` reports ``True``. + + .. code-block:: python + + >>> Some(inner=2).is_none_or(is_even) == True + True + ``Null`` makes ``is_none_or`` report ``True`` without calling the predicate. + + .. code-block:: python + + >>> Null().is_none_or(is_even) == True + True + :: + """ return True def is_some(self) -> bool: + """Papertrail examples: + + ``is_some`` reports ``True`` for a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).is_some() == True + True + For ``Null``, ``is_some`` reports ``False``. + + .. code-block:: python + + >>> Null().is_some() == False + True + :: + """ return False def is_some_and(self, fn: Callable[[T], bool]) -> bool: # noqa: ARG002 + """Papertrail examples: + + When the predicate rejects a ``Some``, ``is_some_and`` reports ``False``. + + .. code-block:: python + + >>> Some(inner=1).is_some_and(is_even) == False + True + When the predicate accepts a ``Some``, ``is_some_and`` reports ``True``. + + .. code-block:: python + + >>> Some(inner=2).is_some_and(is_even) == True + True + A ``Null`` makes ``is_some_and`` report ``False`` without calling the predicate. + + .. code-block:: python + + >>> Null().is_some_and(is_even) == False + True + :: + """ return False def map[U](self, fn: Callable[[T], U]) -> Option[U]: # noqa: ARG002 + """Papertrail examples: + + Mapping a ``Some`` applies the function and wraps its result in a new ``Some``. + + .. code-block:: python + + >>> Some(inner=1).map(add_one) == Some(inner=2) + True + Mapping ``Null`` leaves it unchanged and skips the function. + + .. code-block:: python + + >>> Null().map(add_one) == Null() + True + :: + """ return self def map_or[U](self, default: U, fn: Callable[[T], U]) -> U: # noqa: ARG002 + """Papertrail examples: + + For a ``Some``, ``map_or`` uses the function result instead of the default. + + .. code-block:: python + + >>> Some(inner="foo").map_or(42, len) == 3 + True + For ``Null``, ``map_or`` returns the supplied default. + + .. code-block:: python + + >>> Null().map_or(42, len) == 42 + True + :: + """ return default def map_or_else[U](self, default: Callable[..., U], fn: Callable[[T], U]) -> U: # noqa: ARG002 + """Papertrail examples: + + A ``Some`` makes ``map_or_else`` use the mapping function. + + .. code-block:: python + + >>> Some(inner="foo").map_or_else(get_42, len) == 3 + True + A ``Null`` makes ``map_or_else`` use the default function. + + .. code-block:: python + + >>> Null().map_or_else(get_42, len) == 42 + True + :: + """ return default() def ok_or[E](self, err: E) -> Result[T, E]: + """Papertrail examples: + + A ``Some`` converts to ``Ok`` through ``ok_or``. + + .. code-block:: python + + >>> Some(inner="foo").ok_or(0) == Ok(inner="foo") + True + A ``Null`` converts to ``Err`` through ``ok_or``. + + .. code-block:: python + + >>> Null().ok_or(0) == Err(error=0) + True + :: + """ + from ._result_v2 import Err, Result # noqa: PLC0415 + return cast(Result[T, E], Err[E](err)) def ok_or_else[E](self, err: Callable[[], E]) -> Result[T, E]: + """Papertrail examples: + + A ``Some`` converts to ``Ok`` without calling the error function. + + .. code-block:: python + + >>> Some(inner="foo").ok_or_else(get_42) == Ok(inner="foo") + True + A ``Null`` converts to ``Err`` using the error function result. + + .. code-block:: python + + >>> Null().ok_or_else(get_42) == Err(error=42) + True + :: + """ + from ._result_v2 import Err, Result # noqa: PLC0415 + return cast(Result[T, E], Err[E](err())) def or_(self, opt_b: Option[T]) -> Option[T]: + """Papertrail examples: + + A ``Some`` keeps its value when ``or_`` receives ``Null``. + + .. code-block:: python + + >>> Some(inner=2).or_(Null()) == Some(inner=2) + True + A ``Null`` gives way to a ``Some`` passed to ``or_``. + + .. code-block:: python + + >>> Null().or_(Some(inner=100)) == Some(inner=100) + True + When both options contain values, ``or_`` keeps the first ``Some``. + + .. code-block:: python + + >>> Some(inner=2).or_(Some(inner=100)) == Some(inner=2) + True + When both options are ``Null``, ``or_`` returns ``Null``. + + .. code-block:: python + + >>> Null().or_(Null()) == Null() + True + :: + """ return opt_b def or_else(self, opt_b: Callable[[], Option[T]]) -> Option[T]: + """Papertrail examples: + + A ``Some`` passes through ``or_else`` without calling the function. + + .. code-block:: python + + >>> Some(inner="barbarians").or_else(get_some_vikings) == Some(inner="barbarians") + True + For ``Null``, ``or_else`` returns the ``Some`` produced by the function. + + .. code-block:: python + + >>> Null().or_else(get_some_vikings) == Some(inner="vikings") + True + If the fallback also produces ``Null``, ``or_else`` returns ``Null``. + + .. code-block:: python + + >>> Null().or_else(Null) == Null() + True + :: + """ return opt_b() def replace(self, value: T) -> Option[T]: # noqa: ARG002 + """Papertrail examples: + + Replacing a ``Some`` returns a new ``Some`` with the replacement value. + + .. code-block:: python + + >>> Some(inner=2).replace(5) == Some(inner=5) + True + Replacing ``Null`` leaves it as ``Null``. + + .. code-block:: python + + >>> Null().replace(3) == Null() + True + :: + """ return self def transpose[E](self) -> Result[Option[T], E]: - return Ok(self) + """Papertrail examples: + + Transposing ``Some(Ok(value))`` produces ``Ok(Some(value))``. + + .. code-block:: python + + >>> Some(inner=Ok(inner=2)).transpose() == Ok(inner=Some(inner=2)) + True + Transposing ``Some(Err(error))`` produces ``Err(Some(error))``. + + .. code-block:: python + + >>> Some(inner=Err(error=2)).transpose() == Err(error=Some(inner=2)) + True + Transposing ``Null`` produces ``Ok(Null())``. + + .. code-block:: python + + >>> Null().transpose() == Ok(inner=Null()) + True + :: + """ + from ._result_v2 import Ok, Result # noqa: PLC0415 + + return cast(Result[Option[T], E], Ok(self)) def unwrap(self) -> T: + """Papertrail examples: + + ``unwrap`` extracts the value from a ``Some``. + + .. code-block:: python + + >>> Some(inner=2).unwrap() == 2 + True + :: + """ raise TypeError("Can't call `unwrap` on `Null`") def unwrap_or(self, default: T) -> T: + """Papertrail examples: + + With a ``Some``, ``unwrap_or`` returns the value and ignores the default. + + .. code-block:: python + + >>> Some(inner="car").unwrap_or("bike") == "car" + True + With ``Null``, ``unwrap_or`` returns the default. + + .. code-block:: python + + >>> Null().unwrap_or("bike") == "bike" + True + :: + """ return default def unwrap_or_else(self, fn: Callable[[], T]) -> T: + """Papertrail examples: + + A ``Some`` makes ``unwrap_or_else`` return its value without calling the function. + + .. code-block:: python + + >>> Some(inner=4).unwrap_or_else(get_42) == 4 + True + A ``Null`` makes ``unwrap_or_else`` return the function result. + + .. code-block:: python + + >>> Null().unwrap_or_else(get_42) == 42 + True + :: + """ return fn() def zip[U](self, other: Option[U]) -> Option[tuple[T, U]]: # noqa: ARG002 + """Papertrail examples: + + Zipping two ``Some`` values produces a ``Some`` containing both values. + + .. code-block:: python + + >>> Some(inner=1).zip(Some(inner="hi")) == Some(inner=(1, "hi")) + True + Zipping a ``Some`` with ``Null`` produces ``Null``. + + .. code-block:: python + + >>> Some(inner=1).zip(Null()) == Null() + True + Zipping ``Null`` with a ``Some`` produces ``Null``. + + .. code-block:: python + + >>> Null().zip(Some(inner=1)) == Null() + True + :: + """ return self def unzip[U](self) -> tuple[Option[T], Option[U]]: + """Papertrail examples: + + Unzipping a ``Some`` pair produces two ``Some`` values. + + .. code-block:: python + + >>> Some(inner=(2, 2)).unzip() == (Some(inner=2), Some(inner=2)) + True + Unzipping a ``Some`` with a non-pair value produces two ``Null`` values. + + .. code-block:: python + + >>> Some(inner=4).unzip() == (Null(), Null()) + True + Unzipping ``Null`` produces two ``Null`` values. + + .. code-block:: python + + >>> Null().unzip() == (Null(), Null()) + True + :: + """ return (Null(), Null()) diff --git a/src/danom/_monads/_result.py b/src/danom/_monads/_result.py deleted file mode 100644 index f13adf9..0000000 --- a/src/danom/_monads/_result.py +++ /dev/null @@ -1,302 +0,0 @@ -"""Result monad - -repo-map-desc: A simple Result monad, includes the base Result, Ok and Err. -""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from collections.abc import Callable -from types import TracebackType -from typing import Any, Concatenate, Literal, Never, ParamSpec, Self, TypeVar - -import attrs -from attrs.validators import instance_of - -T_co = TypeVar("T_co", covariant=True) -U_co = TypeVar("U_co", covariant=True) -E_co = TypeVar("E_co", bound=object, covariant=True) -F_co = TypeVar("F_co", bound=object, covariant=True) -P = ParamSpec("P") - -Mappable = Callable[Concatenate[T_co, P], U_co] -Bindable = Callable[Concatenate[T_co, P], "Result[T_co, E_co]"] -Recoverable = Callable[Concatenate[E_co, P], "Result[T_co, E_co]"] - - -@attrs.define(frozen=True) -class Result[T_co, E_co: object](ABC): - """``Result`` monad. Consists of ``Ok`` and ``Err`` for successful and failed operations respectively. - Each monad is a frozen instance to prevent further mutation. - """ - - @classmethod - def unit(cls, inner: T_co) -> Ok[T_co]: - """Unit method. Given an item of type ``T`` return ``Ok(T)`` - - .. doctest:: - - >>> from danom import Err, Ok, Result - - >>> Result.unit(0) == Ok(0) - True - - >>> Ok.unit(0) == Ok(0) - True - - >>> Err.unit(0) == Ok(0) - True - """ - return Ok(inner) - - @abstractmethod - def is_ok(self) -> bool: - """Returns ``True`` if the result type is ``Ok``. - Returns ``False`` if the result type is ``Err``. - - .. doctest:: - - >>> from danom import Err, Ok - - >>> Ok().is_ok() == True - True - - >>> Err().is_ok() == False - True - """ - ... - - @abstractmethod - def map[**P](self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Result[T_co, E_co]: - """Pipe a pure function and wrap the return value with ``Ok``. - Given an ``Err`` will return self. - - .. code-block:: python - - from danom import Err, Ok - - Ok(1).map(add_one) == Ok(2) - Err(error=TypeError()).map(add_one) == Err(error=TypeError()) - """ - ... - - @abstractmethod - def map_err[**P](self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Result[T_co, E_co]: - """Pipe a pure function and wrap the return value with ``Err``. - Given an ``Ok`` will return self. - - .. code-block:: python - - from danom import Err, Ok - - Err(error=TypeError()).map_err(type_err_to_value_err) == Err(error=ValueError()) - Ok(1).map(type_err_to_value_err) == Ok(1) - """ - ... - - @abstractmethod - def and_then[**P]( - self, func: Bindable, *args: P.args, **kwargs: P.kwargs - ) -> Result[T_co, E_co]: - """Pipe another function that returns a monad. For ``Err`` will return original error. - - .. code-block:: python - - from danom import Err, Ok - - Ok(1).and_then(add_one) == Ok(2) - Ok(1).and_then(raise_err) == Err(error=TypeError()) - Err(error=TypeError()).and_then(add_one) == Err(error=TypeError()) - Err(error=TypeError()).and_then(raise_value_err) == Err(error=TypeError()) - """ - ... - - @abstractmethod - def or_else[**P]( - self, func: Recoverable, *args: P.args, **kwargs: P.kwargs - ) -> Result[T_co, E_co]: - """Pipe a function that returns a monad to recover from an ``Err``. For ``Ok`` will return original ``Result``. - - .. code-block:: python - - from danom import Err, Ok - - Ok(1).or_else(replace_err_with_zero) == Ok(1) - Err(error=TypeError()).or_else(replace_err_with_zero) == Ok(0) - """ - ... - - @abstractmethod - def unwrap(self) -> T_co: - """Unwrap the ``Ok`` monad and get the inner value. - Unwrap the ``Err`` monad will raise the inner error. - - .. doctest:: - - >>> from danom import Err, Ok - - >>> Ok().unwrap() == None - True - - >>> Ok(1).unwrap() == 1 - True - - >>> Ok("ok").unwrap() == 'ok' - True - - >>> Err(error=TypeError()).unwrap() - Traceback (most recent call last): - ... - TypeError: - """ - ... - - @staticmethod - def result_is_ok(result: Result[T_co, E_co]) -> bool: - """Check whether the monad is ok. Allows for ``filter`` or ``partition`` in a ``Stream`` without needing a lambda or custom function. - - .. code-block:: python - - from danom import Stream, Result - - Stream.from_iterable([Ok(), Ok(), Err()]).filter(Result.result_is_ok).collect() == (Ok(), Ok()) - - """ - return result.is_ok() - - @staticmethod - def result_unwrap(result: Result[T_co, E_co]) -> T_co: - """Unwrap the ``Ok`` monad and get the inner value. - Unwrap the ``Err`` monad will raise the inner error. - - .. code-block:: python - - from danom import Err, Ok, Stream, Result - - oks, errs = Stream.from_iterable([Ok(1), Ok(2), Err()]).partition(Result.result_is_ok) - oks.map(Result.result_unwrap).collect == (1, 2) - - """ - return result.unwrap() - - def flatten(self) -> Result[T_co, E_co]: - """Flatten the monad. Will return the first ``Err`` or the lowest ``Ok`` instance. - - .. doctest:: - - >>> from danom import Err, Ok, Stream, Result - - >>> Ok(Ok(Ok(1))).flatten() == Ok(1) - True - - >>> Ok(Ok(Err())).flatten() == Err() - True - - """ - current = self - - while True: - if isinstance(current, Ok) and isinstance(current.inner, Result): - current = current.inner - elif isinstance(current, Err) and isinstance(current.error, Result): - current = current.error - else: - return current - - -@attrs.define(frozen=True, hash=True) -class Ok(Result[T_co, Never]): - inner: Any = attrs.field(default=None) - - def is_ok(self) -> Literal[True]: - return True - - def map[**P](self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Ok[T_co]: - return Ok(func(self.inner, *args, **kwargs)) - - def map_err[**P](self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Self: # noqa: ARG002 - return self - - def and_then[**P]( - self, func: Bindable, *args: P.args, **kwargs: P.kwargs - ) -> Result[T_co, E_co]: - return Ok(func(self.inner, *args, **kwargs)).flatten() - - def or_else[**P](self, func: Recoverable, *args: P.args, **kwargs: P.kwargs) -> Self: # noqa: ARG002 - return self - - def unwrap(self) -> T_co: - return self.inner - - -SafeArgs = tuple[tuple[Any, ...], dict[str, Any]] -SafeMethodArgs = tuple[object, tuple[Any, ...], dict[str, Any]] - - -@attrs.define(frozen=True) -class Err(Result[Never, E_co]): - error: Any = attrs.field(default=None) - input_args: tuple[()] | SafeArgs | SafeMethodArgs = attrs.field( - default=(), validator=instance_of(tuple), repr=False - ) - traceback: str = attrs.field(default="", validator=instance_of(str)) - details: list[dict[str, Any]] = attrs.field(factory=list, init=False, repr=False) - - def __attrs_post_init__(self) -> None: - if isinstance(self.error, Exception): - # little hack explained here: https://www.attrs.org/en/stable/init.html#post-init - object.__setattr__(self, "details", self._extract_details(self.error.__traceback__)) - - def _extract_details(self, tb: TracebackType | None) -> list[dict[str, Any]]: - trace_info = [] - while tb: - frame = tb.tb_frame - trace_info.append( - { - "file": frame.f_code.co_filename, - "func": frame.f_code.co_name, - "line_no": tb.tb_lineno, - "locals": frame.f_locals, - } - ) - tb = tb.tb_next - return trace_info - - def is_ok(self) -> Literal[False]: - return False - - def map[**P](self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Self: # noqa: ARG002 - return self - - def map_err[**P](self, func: Mappable, *args: P.args, **kwargs: P.kwargs) -> Err[E_co]: - return Err( - func(self.error, *args, **kwargs), input_args=self.input_args, traceback=self.traceback - ) - - def and_then[**P](self, func: Bindable, *args: P.args, **kwargs: P.kwargs) -> Self: # noqa: ARG002 - return self - - def or_else[**P]( - self, func: Recoverable, *args: P.args, **kwargs: P.kwargs - ) -> Result[T_co, E_co]: # ty: ignore[invalid-method-override] - return Ok(func(self.error, *args, **kwargs)).flatten() - - def unwrap(self) -> T_co: - if isinstance(self.error, Exception): - raise self.error - raise ValueError(f"Err does not have a caught error to raise: {self.error = }") - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Err): - return False - - return all( - ( - type(self.error) is type(other.error), - str(self.error) == str(other.error), - self.input_args == other.input_args, - ) - ) - - def __hash__(self) -> int: - return hash(f"{type(self.error)}{self.error}{self.input_args}") diff --git a/src/danom/_monads/_result_v2.py b/src/danom/_monads/_result_v2.py new file mode 100644 index 0000000..36273f7 --- /dev/null +++ b/src/danom/_monads/_result_v2.py @@ -0,0 +1,1709 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections.abc import Callable +from copy import deepcopy +from typing import TYPE_CHECKING, Any, Concatenate, Never, cast + +import attrs +from attrs.validators import instance_of + +if TYPE_CHECKING: + from ._option import Option + + +@attrs.define(frozen=True) +class Result[T, E](ABC): + @classmethod + def unit(cls, inner: T) -> Result[T, E]: + """Unit method. Given an item of type ``T`` return ``Ok(T)`` + + .. doctest:: + + >>> from danom import Err, Ok, Result + + >>> Result.unit(0) == Ok(0) + True + + >>> Ok.unit(0) == Ok(0) + True + + >>> Err.unit(0) == Ok(0) + True + """ + return cast(Result[T, E], Ok(inner)) + + @staticmethod + def result_is_ok(result: Result[T, E]) -> bool: + """Check whether the monad is ok. Allows for ``filter`` or ``partition`` in a ``Stream`` without needing a lambda or custom function. + + .. code-block:: python + + from danom import Stream, Result + + Stream.from_iterable([Ok(), Ok(), Err()]).filter(Result.result_is_ok).collect() == (Ok(), Ok()) + + """ + return result.is_ok() + + @staticmethod + def result_unwrap(result: Result[T, E]) -> T: + """Unwrap the ``Ok`` monad and get the inner value. + Unwrap the ``Err`` monad will raise the inner error. + + .. code-block:: python + + from danom import Err, Ok, Stream, Result + + oks, errs = Stream.from_iterable([Ok(1), Ok(2), Err()]).partition(Result.result_is_ok) + oks.map(Result.result_unwrap).collect == (1, 2) + + """ + return result.unwrap() + + @abstractmethod + def and_[U, F](self, res: Result[U, F]) -> Result[U, F]: + """Papertrail examples: + + If you call ``and_`` on an ``Ok`` with an ``Err`` as the arg then the ``Err`` takes precedence over the ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).and_(Err(error="late error")) == Err(error="late error") + True + If you call ``and_`` on an ``Err`` with an ``Ok`` as the arg then the ``Err`` still takes precedence over the ``Ok``. + + .. code-block:: python + + >>> Err(error="early error").and_(Ok(inner="foo")) == Err(error="early error") + True + If you call ``and_`` on an ``Err`` with an ``Err`` as the arg then the first ``Err`` is returned and the second is discarded. + + .. code-block:: python + + >>> Err(error="not a 2").and_(Err(error="late error")) == Err(error="not a 2") + True + Whereas, calling ``and_`` on an ``Ok`` with another ``Ok`` as the arg then the first ``Ok`` is discarded and the second is returned. + + .. code-block:: python + + >>> Ok(inner=2).and_(Ok(inner="different result type")) == Ok(inner="different result type") + True + :: + """ + ... + + @abstractmethod + def and_then[U, F, **P]( + self, fn: Callable[Concatenate[T, P], Result[U, F]], *args: P.args, **kwargs: P.kwargs + ) -> Result[U, F]: + """Papertrail examples: + + An ``Ok`` passed to ``and_then`` returns the ``Ok`` produced by the function. + + .. code-block:: python + + >>> Ok(inner=2).and_then(must_be_less_than_10) == Ok(inner=2) + True + When the function passed to ``and_then`` produces an ``Err``, that ``Err`` becomes the result. + + .. code-block:: python + + >>> Ok(inner=20).and_then(must_be_less_than_10) == Err(error="too high") + True + An existing ``Err`` passes through ``and_then`` unchanged, without calling the function. + + .. code-block:: python + + >>> Err(error="not a number").and_then(must_be_less_than_10) == Err(error="not a number") + True + :: + """ + ... + + def cloned(self) -> Result[T, E]: + """Papertrail examples: + + Cloning an ``Ok`` creates a separate ``Ok`` with the same value. + + .. code-block:: python + + >>> Ok(inner=2).cloned() == Ok(inner=2) + True + Cloning an ``Err`` creates a separate ``Err`` with the same value. + + .. code-block:: python + + >>> Err(error=2).cloned() == Err(error=2) + True + :: + """ + return deepcopy(self) + + @abstractmethod + def err(self) -> Option[E]: + """Papertrail examples: + + An ``Ok`` has no error, so ``err`` returns ``Null``. + + .. code-block:: python + + >>> Ok(inner=2).err() == Null() + True + If you call ``err`` on an ``Err`` then ``Some`` with the wrapped error is returned. + + .. code-block:: python + + >>> Err(error="Nothing here").err() == Some(inner="Nothing here") + True + :: + """ + ... + + @abstractmethod + def expect(self, msg: str) -> T: + """Papertrail examples: + + ``expect`` extracts the value from an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).expect("must be positive") == 2 + True + :: + """ + ... + + @abstractmethod + def expect_err(self, msg: str) -> E: + """Papertrail examples: + + ``expect_err`` extracts the error from an ``Err``. + + .. code-block:: python + + >>> Err(error=2).expect_err("must be err") == 2 + True + :: + """ + ... + + @abstractmethod + def flatten(self) -> Result[T, E]: + """Papertrail examples: + + If you call ``flatten`` on three nested ``Ok`` values then two nested ``Ok`` values are returned. Only the outer monad is removed + + .. code-block:: python + + >>> Ok(inner=Ok(inner=Ok(inner=2))).flatten() == Ok(inner=Ok(inner=2)) + True + This is made obvious calling ``flatten`` on a doubly wrapped value returning just the inner monad. + + .. code-block:: python + + >>> Ok(inner=Ok(inner=2)).flatten() == Ok(inner=2) + True + Calling ``flatten`` if the inner is not a monad of the same type is a no-op. + + .. code-block:: python + + >>> Ok(inner=2).flatten() == Ok(inner=2) + True + An ``Err`` passes through ``flatten`` unchanged. + + .. code-block:: python + + >>> Err(error=None).flatten() == Err(error=None) + True + :: + """ + ... + + @abstractmethod + def inspect(self, fn: Callable[[T], None]) -> Result[T, E]: + """Papertrail examples: + + If you call ``inspect`` on an ``Ok`` then the original ``Ok`` is returned after the function is called, this is used for logging or side outputs that shouldn't disrupt the existing flow. + + .. code-block:: python + + >>> Ok(inner=[1]).inspect(append_to_list) == Ok(inner=[1]) + True + If you call ``inspect`` on an ``Err`` then the ``Err`` is returned and the function is not called. + + .. code-block:: python + + >>> Err(error="un-appendable").inspect(append_to_list) == Err(error="un-appendable") + True + :: + """ + ... + + @abstractmethod + def inspect_err(self, fn: Callable[[E], None]) -> Result[T, E]: + """Papertrail examples: + + If you call ``inspect_err`` on an ``Err`` then the original ``Err`` is returned after the function is called, it's essentially the inverse of ``inspect``. + + .. code-block:: python + + >>> Err(error=[1]).inspect_err(append_to_list) == Err(error=[1]) + True + Similarly to calling ``inspect`` on an ``Err``, if you call ``inspect_err`` on an ``Ok`` then the ``Ok`` is returned and the function is not called. + + .. code-block:: python + + >>> Ok(inner="un-appendable").inspect_err(append_to_list) == Ok(inner="un-appendable") + True + :: + """ + ... + + @abstractmethod + def is_err(self) -> bool: + """Papertrail examples: + + ``is_err`` reports ``False`` for an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).is_err() == False + True + For an ``Err``, ``is_err`` reports ``True``. + + .. code-block:: python + + >>> Err(error=2).is_err() == True + True + :: + """ + ... + + @abstractmethod + def is_err_and(self, fn: Callable[[E], bool]) -> bool: + """Papertrail examples: + + ``is_err_and`` requires both the monad to be an ``Err`` and the returned value of the passed in callable to be ``True``, for example, if you called ``is_err_and`` with a function that checks for evenness on an ``Err(1)`` then the result is ``False``. + + .. code-block:: python + + >>> Err(error=1).is_err_and(is_even) == False + True + Therefore, if you call ``is_err_and`` on an ``Err`` with a function that returns ``True`` then ``True`` is returned. + + .. code-block:: python + + >>> Err(error=2).is_err_and(is_even) == True + True + Whereas, if you call ``is_err_and`` on an ``Ok`` with a function then ``False`` is returned and the function is not called. + + .. code-block:: python + + >>> Ok(inner=2).is_err_and(is_even) == False + True + :: + """ + ... + + @abstractmethod + def is_ok(self) -> bool: + """Papertrail examples: + + ``is_ok`` reports ``True`` for an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).is_ok() == True + True + For an ``Err``, ``is_ok`` reports ``False``. + + .. code-block:: python + + >>> Err(error=2).is_ok() == False + True + :: + """ + ... + + @abstractmethod + def is_ok_and(self, fn: Callable[[T], bool]) -> bool: + """Papertrail examples: + + When the predicate rejects an ``Ok``, ``is_ok_and`` reports ``False``. + + .. code-block:: python + + >>> Ok(inner=1).is_ok_and(is_even) == False + True + When the predicate accepts an ``Ok``, ``is_ok_and`` reports ``True``. + + .. code-block:: python + + >>> Ok(inner=2).is_ok_and(is_even) == True + True + An ``Err`` makes ``is_ok_and`` report ``False`` without calling the predicate. + + .. code-block:: python + + >>> Err(error=2).is_ok_and(is_even) == False + True + :: + """ + ... + + @abstractmethod + def map[U, **P]( + self, fn: Callable[Concatenate[T, P], U], *args: P.args, **kwargs: P.kwargs + ) -> Result[U, E]: + """Papertrail examples: + + Mapping an ``Ok`` applies the function and wraps its result in a new ``Ok``. + + .. code-block:: python + + >>> Ok(inner=1).map(add_one) == Ok(inner=2) + True + Mapping an ``Err`` leaves it unchanged and skips the function. + + .. code-block:: python + + >>> Err(error=1).map(add_one) == Err(error=1) + True + :: + """ + ... + + @abstractmethod + def map_err[F, **P]( + self, fn: Callable[Concatenate[E, P], F], *args: P.args, **kwargs: P.kwargs + ) -> Result[T, F]: + """Papertrail examples: + + Mapping an ``Err`` applies the function and wraps its result in a new ``Err``. + + .. code-block:: python + + >>> Err(error=1).map_err(add_one) == Err(error=2) + True + Mapping an ``Ok`` leaves it unchanged and skips the function. + + .. code-block:: python + + >>> Ok(inner=1).map_err(add_one) == Ok(inner=1) + True + :: + """ + ... + + @abstractmethod + def map_or[U, **P]( + self, default: U, fn: Callable[Concatenate[T, P], U], *args: P.args, **kwargs: P.kwargs + ) -> U: + """Papertrail examples: + + For an ``Ok``, ``map_or`` uses the function result instead of the default. + + .. code-block:: python + + >>> Ok(inner="foo").map_or(42, len) == 3 + True + For an ``Err``, ``map_or`` returns the supplied default. + + .. code-block:: python + + >>> Err(error=None).map_or(42, len) == 42 + True + :: + """ + ... + + @abstractmethod + def map_or_else[U, **P]( + self, + default: Callable[P, U], + fn: Callable[Concatenate[T, P], U], + *args: P.args, + **kwargs: P.kwargs, + ) -> U: + """Papertrail examples: + + An ``Ok`` makes ``map_or_else`` use the mapping function. + + .. code-block:: python + + >>> Ok(inner="foo").map_or_else(get_42, len) == 3 + True + An ``Err`` makes ``map_or_else`` use the default function. + + .. code-block:: python + + >>> Err(error=None).map_or_else(get_42, len) == 42 + True + :: + """ + ... + + @abstractmethod + def ok(self) -> Option[T]: + """Papertrail examples: + + An ``Ok`` converts to ``Some`` through ``ok``. + + .. code-block:: python + + >>> Ok(inner=2).ok() == Some(inner=2) + True + An ``Err`` converts to ``Null`` through ``ok``. + + .. code-block:: python + + >>> Err(error=2).ok() == Null() + True + :: + """ + ... + + @abstractmethod + def or_[F](self, res: Result[T, F]) -> Result[T, F]: + """Papertrail examples: + + An ``Ok`` keeps its value when ``or_`` receives an ``Err``. + + .. code-block:: python + + >>> Ok(inner=2).or_(Err(error="foo")) == Ok(inner=2) + True + An ``Err`` gives way to an ``Ok`` passed to ``or_``. + + .. code-block:: python + + >>> Err(error="foo").or_(Ok(inner=100)) == Ok(inner=100) + True + When both values are ``Ok``, ``or_`` keeps the first one. + + .. code-block:: python + + >>> Ok(inner=2).or_(Ok(inner=100)) == Ok(inner=2) + True + When both values are ``Err``, ``or_`` keeps the first error. + + .. code-block:: python + + >>> Err(error="foo").or_(Err(error="foo")) == Err(error="foo") + True + :: + """ + ... + + @abstractmethod + def or_else[F, **P]( + self, fn: Callable[Concatenate[E, P], F], *args: P.args, **kwargs: P.kwargs + ) -> Result[T, F]: + """Papertrail examples: + + An ``Ok`` passes through ``or_else`` without calling the function. + + .. code-block:: python + + >>> Ok(inner="barbarians").or_else(get_ok_vikings) == Ok(inner="barbarians") + True + For an ``Err``, ``or_else`` returns the ``Ok`` produced by the function. + + .. code-block:: python + + >>> Err(error="foo").or_else(get_ok_vikings) == Ok(inner="vikings") + True + If the fallback also produces an ``Err``, ``or_else`` keeps the original error. + + .. code-block:: python + + >>> Err(error="foo").or_else(Err) == Err(error="foo") + True + :: + """ + ... + + @abstractmethod + def transpose(self) -> Option[Result[T, E]]: + """Papertrail examples: + + Transposing ``Ok(Some(value))`` produces ``Some(Ok(value))``. + + .. code-block:: python + + >>> Ok(inner=Some(inner=5)).transpose() == Some(inner=Ok(inner=5)) + True + Transposing ``Ok(Null())`` produces ``Null``. + + .. code-block:: python + + >>> Ok(inner=Null()).transpose() == Null() + True + Transposing an ``Err`` wraps it in ``Some``. + + .. code-block:: python + + >>> Err(error=None).transpose() == Some(inner=Err(error=None)) + True + :: + """ + ... + + @abstractmethod + def unwrap(self) -> T: + """Papertrail examples: + + ``unwrap`` extracts the value from an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).unwrap() == 2 + True + :: + """ + ... + + @abstractmethod + def unwrap_err(self) -> E: + """Papertrail examples: + + ``unwrap_err`` extracts the error from an ``Err``. + + .. code-block:: python + + >>> Err(error="failed").unwrap_err() == "failed" + True + :: + """ + ... + + @abstractmethod + def unwrap_or(self, default: T) -> T: + """Papertrail examples: + + With an ``Ok``, ``unwrap_or`` returns the value and ignores the default. + + .. code-block:: python + + >>> Ok(inner="car").unwrap_or("bike") == "car" + True + With an ``Err``, ``unwrap_or`` returns the default. + + .. code-block:: python + + >>> Err(error=None).unwrap_or("bike") == "bike" + True + :: + """ + ... + + @abstractmethod + def unwrap_or_else(self, fn: Callable[[E], T]) -> T: + """Papertrail examples: + + An ``Ok`` makes ``unwrap_or_else`` return its value without calling the function. + + .. code-block:: python + + >>> Ok(inner=4).unwrap_or_else(get_42) == 4 + True + An ``Err`` makes ``unwrap_or_else`` return the function result. + + .. code-block:: python + + >>> Err(error=None).unwrap_or_else(get_42) == 42 + True + :: + """ + ... + + +@attrs.define(frozen=True) +class Ok[T](Result[T, Never]): + inner: T = attrs.field(default=None) + + def and_[U, E](self, res: Result[U, E]) -> Result[U, E]: + """Papertrail examples: + + If you call ``and_`` on an ``Ok`` with an ``Err`` as the arg then the ``Err`` takes precedence over the ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).and_(Err(error="late error")) == Err(error="late error") + True + If you call ``and_`` on an ``Err`` with an ``Ok`` as the arg then the ``Err`` still takes precedence over the ``Ok``. + + .. code-block:: python + + >>> Err(error="early error").and_(Ok(inner="foo")) == Err(error="early error") + True + If you call ``and_`` on an ``Err`` with an ``Err`` as the arg then the first ``Err`` is returned and the second is discarded. + + .. code-block:: python + + >>> Err(error="not a 2").and_(Err(error="late error")) == Err(error="not a 2") + True + Whereas, calling ``and_`` on an ``Ok`` with another ``Ok`` as the arg then the first ``Ok`` is discarded and the second is returned. + + .. code-block:: python + + >>> Ok(inner=2).and_(Ok(inner="different result type")) == Ok(inner="different result type") + True + :: + """ + return res + + def and_then[U, E, **P]( + self, fn: Callable[Concatenate[T, P], Result[U, E]], *args: P.args, **kwargs: P.kwargs + ) -> Result[U, E]: + """Papertrail examples: + + An ``Ok`` passed to ``and_then`` returns the ``Ok`` produced by the function. + + .. code-block:: python + + >>> Ok(inner=2).and_then(must_be_less_than_10) == Ok(inner=2) + True + When the function passed to ``and_then`` produces an ``Err``, that ``Err`` becomes the result. + + .. code-block:: python + + >>> Ok(inner=20).and_then(must_be_less_than_10) == Err(error="too high") + True + An existing ``Err`` passes through ``and_then`` unchanged, without calling the function. + + .. code-block:: python + + >>> Err(error="not a number").and_then(must_be_less_than_10) == Err(error="not a number") + True + :: + """ + return fn(self.inner, *args, **kwargs) + + def err[E](self) -> Option[E]: + """Papertrail examples: + + An ``Ok`` has no error, so ``err`` returns ``Null``. + + .. code-block:: python + + >>> Ok(inner=2).err() == Null() + True + If you call ``err`` on an ``Err`` then ``Some`` with the wrapped error is returned. + + .. code-block:: python + + >>> Err(error="Nothing here").err() == Some(inner="Nothing here") + True + :: + """ + from ._option import Null # noqa: PLC0415 + + return Null() + + def expect(self, msg: str) -> T: # noqa: ARG002 + """Papertrail examples: + + ``expect`` extracts the value from an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).expect("must be positive") == 2 + True + :: + """ + return self.inner + + def expect_err(self, msg: str) -> Never: + """Papertrail examples: + + ``expect_err`` extracts the error from an ``Err``. + + .. code-block:: python + + >>> Err(error=2).expect_err("must be err") == 2 + True + :: + """ + raise ValueError(msg) + + def flatten(self) -> Result[T, Never]: + """Papertrail examples: + + If you call ``flatten`` on three nested ``Ok`` values then two nested ``Ok`` values are returned. Only the outer monad is removed + + .. code-block:: python + + >>> Ok(inner=Ok(inner=Ok(inner=2))).flatten() == Ok(inner=Ok(inner=2)) + True + This is made obvious calling ``flatten`` on a doubly wrapped value returning just the inner monad. + + .. code-block:: python + + >>> Ok(inner=Ok(inner=2)).flatten() == Ok(inner=2) + True + Calling ``flatten`` if the inner is not a monad of the same type is a no-op. + + .. code-block:: python + + >>> Ok(inner=2).flatten() == Ok(inner=2) + True + An ``Err`` passes through ``flatten`` unchanged. + + .. code-block:: python + + >>> Err(error=None).flatten() == Err(error=None) + True + :: + """ + if isinstance(self.inner, Result): + return cast(Result[T, Never], self.inner) + return cast(Result[T, Never], self) + + def inspect(self, fn: Callable[[T], None]) -> Result[T, Never]: + """Papertrail examples: + + If you call ``inspect`` on an ``Ok`` then the original ``Ok`` is returned after the function is called, this is used for logging or side outputs that shouldn't disrupt the existing flow. + + .. code-block:: python + + >>> Ok(inner=[1]).inspect(append_to_list) == Ok(inner=[1]) + True + If you call ``inspect`` on an ``Err`` then the ``Err`` is returned and the function is not called. + + .. code-block:: python + + >>> Err(error="un-appendable").inspect(append_to_list) == Err(error="un-appendable") + True + :: + """ + fn(deepcopy(self.inner)) + return self + + def inspect_err(self, fn: Callable[[Never], None]) -> Result[T, Never]: # noqa: ARG002 + """Papertrail examples: + + If you call ``inspect_err`` on an ``Err`` then the original ``Err`` is returned after the function is called, it's essentially the inverse of ``inspect``. + + .. code-block:: python + + >>> Err(error=[1]).inspect_err(append_to_list) == Err(error=[1]) + True + Similarly to calling ``inspect`` on an ``Err``, if you call ``inspect_err`` on an ``Ok`` then the ``Ok`` is returned and the function is not called. + + .. code-block:: python + + >>> Ok(inner="un-appendable").inspect_err(append_to_list) == Ok(inner="un-appendable") + True + :: + """ + return self + + def is_err(self) -> bool: + """Papertrail examples: + + ``is_err`` reports ``False`` for an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).is_err() == False + True + For an ``Err``, ``is_err`` reports ``True``. + + .. code-block:: python + + >>> Err(error=2).is_err() == True + True + :: + """ + return False + + def is_err_and(self, fn: Callable[[Never], bool]) -> bool: # noqa: ARG002 + """Papertrail examples: + + ``is_err_and`` requires both the monad to be an ``Err`` and the returned value of the passed in callable to be ``True``, for example, if you called ``is_err_and`` with a function that checks for evenness on an ``Err(1)`` then the result is ``False``. + + .. code-block:: python + + >>> Err(error=1).is_err_and(is_even) == False + True + Therefore, if you call ``is_err_and`` on an ``Err`` with a function that returns ``True`` then ``True`` is returned. + + .. code-block:: python + + >>> Err(error=2).is_err_and(is_even) == True + True + Whereas, if you call ``is_err_and`` on an ``Ok`` with a function then ``False`` is returned and the function is not called. + + .. code-block:: python + + >>> Ok(inner=2).is_err_and(is_even) == False + True + :: + """ + return False + + def is_ok(self) -> bool: + """Papertrail examples: + + ``is_ok`` reports ``True`` for an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).is_ok() == True + True + For an ``Err``, ``is_ok`` reports ``False``. + + .. code-block:: python + + >>> Err(error=2).is_ok() == False + True + :: + """ + return True + + def is_ok_and(self, fn: Callable[[T], bool]) -> bool: + """Papertrail examples: + + When the predicate rejects an ``Ok``, ``is_ok_and`` reports ``False``. + + .. code-block:: python + + >>> Ok(inner=1).is_ok_and(is_even) == False + True + When the predicate accepts an ``Ok``, ``is_ok_and`` reports ``True``. + + .. code-block:: python + + >>> Ok(inner=2).is_ok_and(is_even) == True + True + An ``Err`` makes ``is_ok_and`` report ``False`` without calling the predicate. + + .. code-block:: python + + >>> Err(error=2).is_ok_and(is_even) == False + True + :: + """ + return fn(self.inner) + + def map[U, **P]( + self, fn: Callable[Concatenate[T, P], U], *args: P.args, **kwargs: P.kwargs + ) -> Result[U, Never]: + """Papertrail examples: + + Mapping an ``Ok`` applies the function and wraps its result in a new ``Ok``. + + .. code-block:: python + + >>> Ok(inner=1).map(add_one) == Ok(inner=2) + True + Mapping an ``Err`` leaves it unchanged and skips the function. + + .. code-block:: python + + >>> Err(error=1).map(add_one) == Err(error=1) + True + :: + """ + return Ok(fn(self.inner, *args, **kwargs)) + + def map_err[F, **P]( + self, + fn: Callable[Concatenate[Never, P], F], # noqa: ARG002 + *args: P.args, # noqa: ARG002 + **kwargs: P.kwargs, # noqa: ARG002 + ) -> Result[T, F]: + """Papertrail examples: + + Mapping an ``Err`` applies the function and wraps its result in a new ``Err``. + + .. code-block:: python + + >>> Err(error=1).map_err(add_one) == Err(error=2) + True + Mapping an ``Ok`` leaves it unchanged and skips the function. + + .. code-block:: python + + >>> Ok(inner=1).map_err(add_one) == Ok(inner=1) + True + :: + """ + return cast(Result[T, F], self) + + def map_or[U, **P]( + self, + default: U, # noqa: ARG002 + fn: Callable[Concatenate[T, P], U], + *args: P.args, + **kwargs: P.kwargs, + ) -> U: + """Papertrail examples: + + For an ``Ok``, ``map_or`` uses the function result instead of the default. + + .. code-block:: python + + >>> Ok(inner="foo").map_or(42, len) == 3 + True + For an ``Err``, ``map_or`` returns the supplied default. + + .. code-block:: python + + >>> Err(error=None).map_or(42, len) == 42 + True + :: + """ + return fn(self.inner, *args, **kwargs) + + def map_or_else[U, **P]( + self, + default: Callable[P, U], # noqa: ARG002 + fn: Callable[Concatenate[T, P], U], + *args: P.args, + **kwargs: P.kwargs, + ) -> U: + """Papertrail examples: + + An ``Ok`` makes ``map_or_else`` use the mapping function. + + .. code-block:: python + + >>> Ok(inner="foo").map_or_else(get_42, len) == 3 + True + An ``Err`` makes ``map_or_else`` use the default function. + + .. code-block:: python + + >>> Err(error=None).map_or_else(get_42, len) == 42 + True + :: + """ + return fn(self.inner, *args, **kwargs) + + def ok(self) -> Option[T]: + """Papertrail examples: + + An ``Ok`` converts to ``Some`` through ``ok``. + + .. code-block:: python + + >>> Ok(inner=2).ok() == Some(inner=2) + True + An ``Err`` converts to ``Null`` through ``ok``. + + .. code-block:: python + + >>> Err(error=2).ok() == Null() + True + :: + """ + from ._option import Some # noqa: PLC0415 + + return Some(self.inner) + + def or_[F](self, res: Result[T, F]) -> Result[T, F]: # noqa: ARG002 + """Papertrail examples: + + An ``Ok`` keeps its value when ``or_`` receives an ``Err``. + + .. code-block:: python + + >>> Ok(inner=2).or_(Err(error="foo")) == Ok(inner=2) + True + An ``Err`` gives way to an ``Ok`` passed to ``or_``. + + .. code-block:: python + + >>> Err(error="foo").or_(Ok(inner=100)) == Ok(inner=100) + True + When both values are ``Ok``, ``or_`` keeps the first one. + + .. code-block:: python + + >>> Ok(inner=2).or_(Ok(inner=100)) == Ok(inner=2) + True + When both values are ``Err``, ``or_`` keeps the first error. + + .. code-block:: python + + >>> Err(error="foo").or_(Err(error="foo")) == Err(error="foo") + True + :: + """ + return cast(Result[T, F], self) + + def or_else[F, **P]( + self, + fn: Callable[Concatenate[Never, P], F], # noqa: ARG002 + *args: P.args, # noqa: ARG002 + **kwargs: P.kwargs, # noqa: ARG002 + ) -> Result[T, F]: + """Papertrail examples: + + An ``Ok`` passes through ``or_else`` without calling the function. + + .. code-block:: python + + >>> Ok(inner="barbarians").or_else(get_ok_vikings) == Ok(inner="barbarians") + True + For an ``Err``, ``or_else`` returns the ``Ok`` produced by the function. + + .. code-block:: python + + >>> Err(error="foo").or_else(get_ok_vikings) == Ok(inner="vikings") + True + If the fallback also produces an ``Err``, ``or_else`` keeps the original error. + + .. code-block:: python + + >>> Err(error="foo").or_else(Err) == Err(error="foo") + True + :: + """ + return cast(Result[T, F], self) + + def transpose(self) -> Option[Result[T, Never]]: + """Papertrail examples: + + Transposing ``Ok(Some(value))`` produces ``Some(Ok(value))``. + + .. code-block:: python + + >>> Ok(inner=Some(inner=5)).transpose() == Some(inner=Ok(inner=5)) + True + Transposing ``Ok(Null())`` produces ``Null``. + + .. code-block:: python + + >>> Ok(inner=Null()).transpose() == Null() + True + Transposing an ``Err`` wraps it in ``Some``. + + .. code-block:: python + + >>> Err(error=None).transpose() == Some(inner=Err(error=None)) + True + :: + """ + from ._option import Null, Some # noqa: PLC0415 + + if isinstance(self.inner, Some): + return Some(Ok(self.inner.inner)) + if isinstance(self.inner, Null): + return Null() + raise TypeError("inner must be an `Option` type") + + def unwrap(self) -> T: + """Papertrail examples: + + ``unwrap`` extracts the value from an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).unwrap() == 2 + True + :: + """ + return self.inner + + def unwrap_err(self) -> Never: + """Papertrail examples: + + ``unwrap_err`` extracts the error from an ``Err``. + + .. code-block:: python + + >>> Err(error="failed").unwrap_err() == "failed" + True + :: + """ + raise TypeError("Can't call `unwrap_err` on `Ok`") + + def unwrap_or(self, default: T) -> T: # noqa: ARG002 + """Papertrail examples: + + With an ``Ok``, ``unwrap_or`` returns the value and ignores the default. + + .. code-block:: python + + >>> Ok(inner="car").unwrap_or("bike") == "car" + True + With an ``Err``, ``unwrap_or`` returns the default. + + .. code-block:: python + + >>> Err(error=None).unwrap_or("bike") == "bike" + True + :: + """ + return self.inner + + def unwrap_or_else(self, fn: Callable[[Never], T]) -> T: # noqa: ARG002 + """Papertrail examples: + + An ``Ok`` makes ``unwrap_or_else`` return its value without calling the function. + + .. code-block:: python + + >>> Ok(inner=4).unwrap_or_else(get_42) == 4 + True + An ``Err`` makes ``unwrap_or_else`` return the function result. + + .. code-block:: python + + >>> Err(error=None).unwrap_or_else(get_42) == 42 + True + :: + """ + return self.inner + + +SafeArgs = tuple[tuple[Any, ...], dict[str, Any]] +SafeMethodArgs = tuple[object, tuple[Any, ...], dict[str, Any]] + + +@attrs.define(frozen=True) +class Err[E](Result[Never, E]): + error: E = attrs.field(default=None) + input_args: tuple[()] | SafeArgs | SafeMethodArgs = attrs.field( + default=(), validator=instance_of(tuple), repr=False + ) + traceback: str = attrs.field(default="", validator=instance_of(str), repr=False) + + def and_[U, F](self, res: Result[U, F]) -> Result[U, F]: # noqa: ARG002 + """Papertrail examples: + + If you call ``and_`` on an ``Ok`` with an ``Err`` as the arg then the ``Err`` takes precedence over the ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).and_(Err(error="late error")) == Err(error="late error") + True + If you call ``and_`` on an ``Err`` with an ``Ok`` as the arg then the ``Err`` still takes precedence over the ``Ok``. + + .. code-block:: python + + >>> Err(error="early error").and_(Ok(inner="foo")) == Err(error="early error") + True + If you call ``and_`` on an ``Err`` with an ``Err`` as the arg then the first ``Err`` is returned and the second is discarded. + + .. code-block:: python + + >>> Err(error="not a 2").and_(Err(error="late error")) == Err(error="not a 2") + True + Whereas, calling ``and_`` on an ``Ok`` with another ``Ok`` as the arg then the first ``Ok`` is discarded and the second is returned. + + .. code-block:: python + + >>> Ok(inner=2).and_(Ok(inner="different result type")) == Ok(inner="different result type") + True + :: + """ + return cast(Result[U, F], self) + + def and_then[U, F, **P]( + self, + fn: Callable[Concatenate[Never, P], Result[U, F]], # noqa: ARG002 + *args: P.args, # noqa: ARG002 + **kwargs: P.kwargs, # noqa: ARG002 + ) -> Result[U, F]: + """Papertrail examples: + + An ``Ok`` passed to ``and_then`` returns the ``Ok`` produced by the function. + + .. code-block:: python + + >>> Ok(inner=2).and_then(must_be_less_than_10) == Ok(inner=2) + True + When the function passed to ``and_then`` produces an ``Err``, that ``Err`` becomes the result. + + .. code-block:: python + + >>> Ok(inner=20).and_then(must_be_less_than_10) == Err(error="too high") + True + An existing ``Err`` passes through ``and_then`` unchanged, without calling the function. + + .. code-block:: python + + >>> Err(error="not a number").and_then(must_be_less_than_10) == Err(error="not a number") + True + :: + """ + return cast(Result[U, F], self) + + def err(self) -> Option[E]: + """Papertrail examples: + + An ``Ok`` has no error, so ``err`` returns ``Null``. + + .. code-block:: python + + >>> Ok(inner=2).err() == Null() + True + If you call ``err`` on an ``Err`` then ``Some`` with the wrapped error is returned. + + .. code-block:: python + + >>> Err(error="Nothing here").err() == Some(inner="Nothing here") + True + :: + """ + from ._option import Some # noqa: PLC0415 + + return Some(self.error) + + def expect(self, msg: str) -> Never: + """Papertrail examples: + + ``expect`` extracts the value from an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).expect("must be positive") == 2 + True + :: + """ + raise ValueError(msg) + + def expect_err(self, msg: str) -> E: # noqa: ARG002 + """Papertrail examples: + + ``expect_err`` extracts the error from an ``Err``. + + .. code-block:: python + + >>> Err(error=2).expect_err("must be err") == 2 + True + :: + """ + return self.error + + def flatten(self) -> Result[Never, E]: + """Papertrail examples: + + If you call ``flatten`` on three nested ``Ok`` values then two nested ``Ok`` values are returned. Only the outer monad is removed + + .. code-block:: python + + >>> Ok(inner=Ok(inner=Ok(inner=2))).flatten() == Ok(inner=Ok(inner=2)) + True + This is made obvious calling ``flatten`` on a doubly wrapped value returning just the inner monad. + + .. code-block:: python + + >>> Ok(inner=Ok(inner=2)).flatten() == Ok(inner=2) + True + Calling ``flatten`` if the inner is not a monad of the same type is a no-op. + + .. code-block:: python + + >>> Ok(inner=2).flatten() == Ok(inner=2) + True + An ``Err`` passes through ``flatten`` unchanged. + + .. code-block:: python + + >>> Err(error=None).flatten() == Err(error=None) + True + :: + """ + return self + + def inspect(self, fn: Callable[[Never], None]) -> Result[Never, E]: # noqa: ARG002 + """Papertrail examples: + + If you call ``inspect`` on an ``Ok`` then the original ``Ok`` is returned after the function is called, this is used for logging or side outputs that shouldn't disrupt the existing flow. + + .. code-block:: python + + >>> Ok(inner=[1]).inspect(append_to_list) == Ok(inner=[1]) + True + If you call ``inspect`` on an ``Err`` then the ``Err`` is returned and the function is not called. + + .. code-block:: python + + >>> Err(error="un-appendable").inspect(append_to_list) == Err(error="un-appendable") + True + :: + """ + return self + + def inspect_err(self, fn: Callable[[E], None]) -> Result[Never, E]: + """Papertrail examples: + + If you call ``inspect_err`` on an ``Err`` then the original ``Err`` is returned after the function is called, it's essentially the inverse of ``inspect``. + + .. code-block:: python + + >>> Err(error=[1]).inspect_err(append_to_list) == Err(error=[1]) + True + Similarly to calling ``inspect`` on an ``Err``, if you call ``inspect_err`` on an ``Ok`` then the ``Ok`` is returned and the function is not called. + + .. code-block:: python + + >>> Ok(inner="un-appendable").inspect_err(append_to_list) == Ok(inner="un-appendable") + True + :: + """ + fn(deepcopy(self.error)) + return self + + def is_err(self) -> bool: + """Papertrail examples: + + ``is_err`` reports ``False`` for an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).is_err() == False + True + For an ``Err``, ``is_err`` reports ``True``. + + .. code-block:: python + + >>> Err(error=2).is_err() == True + True + :: + """ + return True + + def is_err_and(self, fn: Callable[[E], bool]) -> bool: + """Papertrail examples: + + ``is_err_and`` requires both the monad to be an ``Err`` and the returned value of the passed in callable to be ``True``, for example, if you called ``is_err_and`` with a function that checks for evenness on an ``Err(1)`` then the result is ``False``. + + .. code-block:: python + + >>> Err(error=1).is_err_and(is_even) == False + True + Therefore, if you call ``is_err_and`` on an ``Err`` with a function that returns ``True`` then ``True`` is returned. + + .. code-block:: python + + >>> Err(error=2).is_err_and(is_even) == True + True + Whereas, if you call ``is_err_and`` on an ``Ok`` with a function then ``False`` is returned and the function is not called. + + .. code-block:: python + + >>> Ok(inner=2).is_err_and(is_even) == False + True + :: + """ + return fn(self.error) + + def is_ok(self) -> bool: + """Papertrail examples: + + ``is_ok`` reports ``True`` for an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).is_ok() == True + True + For an ``Err``, ``is_ok`` reports ``False``. + + .. code-block:: python + + >>> Err(error=2).is_ok() == False + True + :: + """ + return False + + def is_ok_and(self, fn: Callable[[Never], bool]) -> bool: # noqa: ARG002 + """Papertrail examples: + + When the predicate rejects an ``Ok``, ``is_ok_and`` reports ``False``. + + .. code-block:: python + + >>> Ok(inner=1).is_ok_and(is_even) == False + True + When the predicate accepts an ``Ok``, ``is_ok_and`` reports ``True``. + + .. code-block:: python + + >>> Ok(inner=2).is_ok_and(is_even) == True + True + An ``Err`` makes ``is_ok_and`` report ``False`` without calling the predicate. + + .. code-block:: python + + >>> Err(error=2).is_ok_and(is_even) == False + True + :: + """ + return False + + def map[U, **P]( + self, + fn: Callable[Concatenate[Never, P], U], # noqa: ARG002 + *args: P.args, # noqa: ARG002 + **kwargs: P.kwargs, # noqa: ARG002 + ) -> Result[U, E]: + """Papertrail examples: + + Mapping an ``Ok`` applies the function and wraps its result in a new ``Ok``. + + .. code-block:: python + + >>> Ok(inner=1).map(add_one) == Ok(inner=2) + True + Mapping an ``Err`` leaves it unchanged and skips the function. + + .. code-block:: python + + >>> Err(error=1).map(add_one) == Err(error=1) + True + :: + """ + return cast(Result[U, E], self) + + def map_err[F, **P]( + self, fn: Callable[Concatenate[E, P], F], *args: P.args, **kwargs: P.kwargs + ) -> Result[Never, F]: + """Papertrail examples: + + Mapping an ``Err`` applies the function and wraps its result in a new ``Err``. + + .. code-block:: python + + >>> Err(error=1).map_err(add_one) == Err(error=2) + True + Mapping an ``Ok`` leaves it unchanged and skips the function. + + .. code-block:: python + + >>> Ok(inner=1).map_err(add_one) == Ok(inner=1) + True + :: + """ + return Err( + fn(self.error, *args, **kwargs), input_args=self.input_args, traceback=self.traceback + ) + + def map_or[U, **P]( + self, + default: U, + fn: Callable[Concatenate[Never, P], U], # noqa: ARG002 + *args: P.args, # noqa: ARG002 + **kwargs: P.kwargs, # noqa: ARG002 + ) -> U: + """Papertrail examples: + + For an ``Ok``, ``map_or`` uses the function result instead of the default. + + .. code-block:: python + + >>> Ok(inner="foo").map_or(42, len) == 3 + True + For an ``Err``, ``map_or`` returns the supplied default. + + .. code-block:: python + + >>> Err(error=None).map_or(42, len) == 42 + True + :: + """ + return default + + def map_or_else[U, **P]( + self, + default: Callable[P, U], + fn: Callable[Concatenate[Never, P], U], # noqa: ARG002 + *args: P.args, + **kwargs: P.kwargs, + ) -> U: + """Papertrail examples: + + An ``Ok`` makes ``map_or_else`` use the mapping function. + + .. code-block:: python + + >>> Ok(inner="foo").map_or_else(get_42, len) == 3 + True + An ``Err`` makes ``map_or_else`` use the default function. + + .. code-block:: python + + >>> Err(error=None).map_or_else(get_42, len) == 42 + True + :: + """ + return default(*args, **kwargs) + + def ok(self) -> Option[Never]: + """Papertrail examples: + + An ``Ok`` converts to ``Some`` through ``ok``. + + .. code-block:: python + + >>> Ok(inner=2).ok() == Some(inner=2) + True + An ``Err`` converts to ``Null`` through ``ok``. + + .. code-block:: python + + >>> Err(error=2).ok() == Null() + True + :: + """ + from ._option import Null # noqa: PLC0415 + + return Null() + + def or_[F](self, res: Result[Never, F]) -> Result[Never, F]: + """Papertrail examples: + + An ``Ok`` keeps its value when ``or_`` receives an ``Err``. + + .. code-block:: python + + >>> Ok(inner=2).or_(Err(error="foo")) == Ok(inner=2) + True + An ``Err`` gives way to an ``Ok`` passed to ``or_``. + + .. code-block:: python + + >>> Err(error="foo").or_(Ok(inner=100)) == Ok(inner=100) + True + When both values are ``Ok``, ``or_`` keeps the first one. + + .. code-block:: python + + >>> Ok(inner=2).or_(Ok(inner=100)) == Ok(inner=2) + True + When both values are ``Err``, ``or_`` keeps the first error. + + .. code-block:: python + + >>> Err(error="foo").or_(Err(error="foo")) == Err(error="foo") + True + :: + """ + return res + + def or_else[F, **P]( + self, fn: Callable[Concatenate[E, P], F], *args: P.args, **kwargs: P.kwargs + ) -> Result[Never, F]: + """Papertrail examples: + + An ``Ok`` passes through ``or_else`` without calling the function. + + .. code-block:: python + + >>> Ok(inner="barbarians").or_else(get_ok_vikings) == Ok(inner="barbarians") + True + For an ``Err``, ``or_else`` returns the ``Ok`` produced by the function. + + .. code-block:: python + + >>> Err(error="foo").or_else(get_ok_vikings) == Ok(inner="vikings") + True + If the fallback also produces an ``Err``, ``or_else`` keeps the original error. + + .. code-block:: python + + >>> Err(error="foo").or_else(Err) == Err(error="foo") + True + :: + """ + return cast(Result[Never, F], fn(self.error, *args, **kwargs)) + + def transpose(self) -> Option[Result[Never, E]]: + """Papertrail examples: + + Transposing ``Ok(Some(value))`` produces ``Some(Ok(value))``. + + .. code-block:: python + + >>> Ok(inner=Some(inner=5)).transpose() == Some(inner=Ok(inner=5)) + True + Transposing ``Ok(Null())`` produces ``Null``. + + .. code-block:: python + + >>> Ok(inner=Null()).transpose() == Null() + True + Transposing an ``Err`` wraps it in ``Some``. + + .. code-block:: python + + >>> Err(error=None).transpose() == Some(inner=Err(error=None)) + True + :: + """ + from ._option import Some # noqa: PLC0415 + + return Some(self) + + def unwrap(self) -> Never: + """Papertrail examples: + + ``unwrap`` extracts the value from an ``Ok``. + + .. code-block:: python + + >>> Ok(inner=2).unwrap() == 2 + True + :: + """ + if isinstance(self.error, Exception): + raise self.error + raise TypeError("Can't call `unwrap` on `Err`") + + def unwrap_err(self) -> E: + """Papertrail examples: + + ``unwrap_err`` extracts the error from an ``Err``. + + .. code-block:: python + + >>> Err(error="failed").unwrap_err() == "failed" + True + :: + """ + return self.error + + def unwrap_or[U](self, default: U) -> U: + """Papertrail examples: + + With an ``Ok``, ``unwrap_or`` returns the value and ignores the default. + + .. code-block:: python + + >>> Ok(inner="car").unwrap_or("bike") == "car" + True + With an ``Err``, ``unwrap_or`` returns the default. + + .. code-block:: python + + >>> Err(error=None).unwrap_or("bike") == "bike" + True + :: + """ + return default + + def unwrap_or_else[U](self, fn: Callable[[E], U]) -> U: + """Papertrail examples: + + An ``Ok`` makes ``unwrap_or_else`` return its value without calling the function. + + .. code-block:: python + + >>> Ok(inner=4).unwrap_or_else(get_42) == 4 + True + An ``Err`` makes ``unwrap_or_else`` return the function result. + + .. code-block:: python + + >>> Err(error=None).unwrap_or_else(get_42) == 42 + True + :: + """ + return fn(self.error) diff --git a/src/danom/_monads/_safe.py b/src/danom/_monads/_safe.py index bfcccd7..ace2b24 100644 --- a/src/danom/_monads/_safe.py +++ b/src/danom/_monads/_safe.py @@ -8,7 +8,7 @@ from collections.abc import Callable from typing import Concatenate, ParamSpec, TypeVar, overload -from ._result import Err, Ok, Result +from ._result_v2 import Err, Ok, Result T = TypeVar("T") P = ParamSpec("P") @@ -65,7 +65,7 @@ def decorator(func: Callable[P, U]) -> Callable[P, Result[U, Exception]]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[U, Exception]: try: - return Ok(func(*args, **kwargs)) + return Ok(func(*args, **kwargs)) # ty: ignore[invalid-return-type] except errors as e: return Err(error=e, input_args=(args, kwargs), traceback=traceback.format_exc()) # ty: ignore[invalid-return-type] @@ -97,7 +97,7 @@ def add_one(self, a: int) -> int: @functools.wraps(func) def wrapper(self: T, *args: P.args, **kwargs: P.kwargs) -> Result[U, Exception]: try: - return Ok(func(self, *args, **kwargs)) + return Ok(func(self, *args, **kwargs)) # ty: ignore[invalid-return-type] except Exception as e: # noqa: BLE001 return Err(error=e, input_args=(self, args, kwargs), traceback=traceback.format_exc()) # ty: ignore[invalid-return-type] diff --git a/src/danom/_stream/_par.py b/src/danom/_stream/_par.py index 4b51f01..2586a6a 100644 --- a/src/danom/_stream/_par.py +++ b/src/danom/_stream/_par.py @@ -56,7 +56,7 @@ def collect(self, *, workers: int = 4, use_threads: bool = False) -> tuple[U, .. def to_stream(self) -> Stream[T]: """Convert the ``ParStream`` to a synchronous ``Stream``.""" - from ._sync import Stream + from ._sync import Stream # noqa: PLC0415 return Stream(self.seq, self.ops) diff --git a/src/danom/_stream/_sync.py b/src/danom/_stream/_sync.py index 9956196..57fd7a1 100644 --- a/src/danom/_stream/_sync.py +++ b/src/danom/_stream/_sync.py @@ -131,7 +131,7 @@ def collect(self, *, workers: int = 4, use_threads: bool = False) -> tuple[U, .. def to_par(self) -> ParStream[T]: """Convert the ``Stream`` to a ``ParStream``.""" - from ._par import ParStream + from ._par import ParStream # noqa: PLC0415 return ParStream(self.seq, self.ops) diff --git a/tests/conftest.py b/tests/conftest.py index fab178d..b27e9e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,29 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Generator from multiprocessing.managers import ListProxy from pathlib import Path -from typing import Any, NoReturn, Self +from typing import Any, Literal, NoReturn, Self, cast + +import pytest from danom import Err, Ok, Result, safe, safe_method +from danom._monads._option import Some +from dev_tools.create_examples.collection.recorder import _RECORDER +from dev_tools.create_examples.transformation.transform import update_modified_docstrings REPO_ROOT = Path(__file__).parents[1] +@pytest.fixture(scope="session", autouse=True) +def collect_examples() -> Generator[Any, None, None]: + yield + _RECORDER.path = REPO_ROOT / ".papertrail_cache/examples.json" + _RECORDER.prepare_files().write_examples() + update_modified_docstrings(_RECORDER.path) + + def make_async(fn: Callable[..., Any]) -> Callable[..., Awaitable[Any]]: async def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 return fn(*args, **kwargs) @@ -98,9 +111,9 @@ def safe_add(a: int, b: int) -> int: def safe_add_one(x: float | str) -> Result[float | str, TypeError]: if isinstance(x, (int, float)): - return Ok(x + 1) + return cast(Result[float | str, TypeError], Ok(x + 1)) if isinstance(x, str): - return Ok(x + "1") + return cast(Result[float | str, TypeError], Ok(x + "1")) return Err(TypeError(f"unsupported type: {type(x)}")) @@ -124,6 +137,18 @@ def div_zero(x: int) -> float: return x / 0 +def get_some_vikings() -> Some[str]: + return Some("vikings") + + +def get_ok_vikings(*args) -> Ok[str]: # noqa: ARG001 ANN002 + return Ok("vikings") + + +def get_42(*args) -> Literal[42]: # noqa: ARG001 ANN002 + return 42 + + class Adder: def __init__(self) -> None: self.result = 0 diff --git a/tests/monads/test_either.py b/tests/monads/test_either.py index ab5b29a..3b9a599 100644 --- a/tests/monads/test_either.py +++ b/tests/monads/test_either.py @@ -1,6 +1,7 @@ import pytest from danom import Either, Left, Right +from dev_tools.create_examples.collection.example import example from tests.conftest import add_one @@ -50,7 +51,7 @@ def test_unwrap(monad, inner): ("monad", "expected_result"), [pytest.param(Right(), True), pytest.param(Left(), False)] ) def test_is_ok(monad, expected_result): - assert monad.is_ok() == expected_result + assert example(monad.is_ok) == expected_result @pytest.mark.parametrize( @@ -58,7 +59,7 @@ def test_is_ok(monad, expected_result): [pytest.param(Right(0), add_one, Right(1)), pytest.param(Left(), add_one, Left())], ) def test_map(monad, func, expected_result): - assert monad.map(func) == expected_result + assert example(monad.map, func) == expected_result @pytest.mark.parametrize( @@ -66,7 +67,7 @@ def test_map(monad, func, expected_result): [pytest.param(Right(0), add_one, Right(0)), pytest.param(Left(0), add_one, Left(1))], ) def test_map_err(monad, func, expected_result): - assert monad.map_err(func) == expected_result + assert example(monad.map_err, func) == expected_result @pytest.mark.parametrize( @@ -94,4 +95,4 @@ def test_staticmethod_result_unwrap(monad, inner): ], ) def test_flatten(monad, expected_result) -> None: - assert monad.flatten() == expected_result + assert example(monad.flatten) == expected_result diff --git a/tests/monads/test_monad_laws.py b/tests/monads/test_monad_laws.py index c694f61..f85f2cf 100644 --- a/tests/monads/test_monad_laws.py +++ b/tests/monads/test_monad_laws.py @@ -1,10 +1,9 @@ from __future__ import annotations -import pytest from hypothesis import given from hypothesis import strategies as st -from danom import Either, Err, Left, Ok, Result, Right, identity +from danom import Either, Err, Left, Ok, Result, Right def monad_tests( @@ -30,39 +29,16 @@ def test_monadic_associativity(monad, f, g, h) -> None: ).or_else(h) st_results = st.integers().map(ok_monad) | st.text().map(err_monad) - st_nested_results = st.recursive( - st_results, lambda children: st.one_of(children.map(ok_monad)), max_leaves=5 - ) - st_nested_errs = st.recursive( - st_results, lambda children: st.one_of(children.map(err_monad)), max_leaves=5 - ) - - @given(monad=st_nested_results) - def test_flatten_idempotent(monad) -> None: - assert monad.flatten().flatten() == monad.flatten() @given(monad=st_results) def test_flatten_noop_for_flat_monad(monad) -> None: assert monad.flatten() == monad - @given(monad=st_nested_results) - def test_and_then_flattens(monad) -> None: - assert monad.flatten() == monad.and_then(identity) - - @given(monad=st_nested_errs) - def test_or_else_flattens(monad) -> None: - if isinstance(monad, Result): - pytest.skip("or_else flatten law not defined for Result") - assert monad.flatten() == monad.or_else(identity) - return ( test_monadic_left_identity, test_monadic_right_identity, test_monadic_associativity, - test_flatten_idempotent, test_flatten_noop_for_flat_monad, - test_and_then_flattens, - test_or_else_flattens, ) @@ -70,10 +46,7 @@ def test_or_else_flattens(monad) -> None: test_result_left_identity, test_result_right_identity, test_result_associativity, - test_result_flatten_idempotent, test_result_flatten_noop_for_flat_monad, - test_result_and_then_flattens, - test_result_or_else_flattens, ) = monad_tests(Result, Ok, Err) @@ -81,8 +54,5 @@ def test_or_else_flattens(monad) -> None: test_either_left_identity, test_either_right_identity, test_either_associativity, - test_either_flatten_idempotent, test_either_flatten_noop_for_flat_monad, - test_either_and_then_flattens, - test_either_or_else_flattens, ) = monad_tests(Either, Right, Left) diff --git a/tests/monads/test_option.py b/tests/monads/test_option.py index 8ae09f0..d737b70 100644 --- a/tests/monads/test_option.py +++ b/tests/monads/test_option.py @@ -3,20 +3,41 @@ import pytest from danom import Err, Null, Ok, Option, Some -from tests.conftest import add_one, is_even +from dev_tools.create_examples.collection.example import example +from tests.conftest import add_one, get_42, get_some_vikings, is_even @pytest.mark.parametrize( ("monad", "opt_b", "expected_result"), [ - pytest.param(Some(2), Null(), Null()), - pytest.param(Null(), Some("foo"), Null()), - pytest.param(Some(2), Some("foo"), Some("foo")), - pytest.param(Null(), Null(), Null()), + pytest.param( + Some(2), + Null(), + Null(), + id="A ``Some`` combined with ``Null`` through ``and_`` produces ``Null``.", + ), + pytest.param( + Null(), + Some("foo"), + Null(), + id="A ``Null`` combined with an ``Some`` through ``and_`` remains ``Null``.", + ), + pytest.param( + Some(2), + Some("foo"), + Some("foo"), + id="When both options contain values, ``and_`` keeps the second ``Some``.", + ), + pytest.param( + Null(), + Null(), + Null(), + id="Combining two ``Null`` values with ``and_`` produces ``Null``.", + ), ], ) -def test_and_(monad: Option, opt_b, expected_result) -> None: - assert monad.and_(opt_b) == expected_result +def test_and_(request, monad: Option, opt_b, expected_result) -> None: + assert example(monad.and_, opt_b, description=request.node.callspec.id) == expected_result def must_be_less_than_10(x: int) -> Option[int]: @@ -26,72 +47,137 @@ def must_be_less_than_10(x: int) -> Option[int]: @pytest.mark.parametrize( ("monad", "fn", "expected_result"), [ - pytest.param(Some(2), must_be_less_than_10, Some(2)), - pytest.param(Some(20), must_be_less_than_10, Null()), - pytest.param(Null(), must_be_less_than_10, Null()), + pytest.param( + Some(2), + must_be_less_than_10, + Some(2), + id="A successful function passed to ``and_then`` returns its ``Some`` result.", + ), + pytest.param( + Some(20), + must_be_less_than_10, + Null(), + id="When the function returns ``Null``, ``and_then`` passes that ``Null`` through.", + ), + pytest.param( + Null(), + must_be_less_than_10, + Null(), + id="An existing ``Null`` skips the function passed to ``and_then``.", + ), ], ) -def test_and_then(monad: Option, fn, expected_result) -> None: - assert monad.and_then(fn) == expected_result +def test_and_then(request, monad: Option, fn, expected_result) -> None: + assert example(monad.and_then, fn, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( - ("monad", "expected_result"), [pytest.param(Some(2), [2]), pytest.param(Null(), [])] + ("monad", "expected_result"), + [ + pytest.param(Some(2), [2], id="A ``Some`` becomes a one-item list through ``as_list``."), + pytest.param(Null(), [], id="``as_list`` represents ``Null`` as an empty list."), + ], ) -def test_as_list(monad: Option, expected_result) -> None: - assert monad.as_list() == expected_result +def test_as_list(request, monad: Option, expected_result) -> None: + assert example(monad.as_list, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( - ("monad", "expected_result"), [pytest.param(Some(2), (2,)), pytest.param(Null(), ())] + ("monad", "expected_result"), + [ + pytest.param(Some(2), (2,), id="A ``Some`` becomes a one-item tuple through ``as_tuple``."), + pytest.param(Null(), (), id="``as_tuple`` represents ``Null`` as an empty tuple."), + ], ) -def test_as_tuple(monad: Option, expected_result) -> None: - assert monad.as_tuple() == expected_result +def test_as_tuple(request, monad: Option, expected_result) -> None: + assert example(monad.as_tuple, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( - ("monad", "expected_result"), [pytest.param(Some(2), Some(2)), pytest.param(Null(), Null())] + ("monad", "expected_result"), + [ + pytest.param( + Some(2), Some(2), id="Cloning a ``Some`` creates a separate option with the same value." + ), + pytest.param(Null(), Null(), id="Cloning ``Null`` creates a separate ``Null``."), + ], ) -def test_cloned(monad: Option, expected_result) -> None: - assert monad.cloned() == expected_result +def test_cloned(request, monad: Option, expected_result) -> None: + assert example(monad.cloned, description=request.node.callspec.id) == expected_result assert id(monad) != id(expected_result) @pytest.mark.parametrize( ("monad", "msg", "expected_result", "expected_context"), [ - pytest.param(Some(2), "must be positive", 2, nullcontext()), - pytest.param(Null(), "must be positive", None, pytest.raises(ValueError)), + pytest.param( + Some(2), + "must be positive", + 2, + nullcontext(), + id="``expect`` extracts the value from a ``Some``.", + ), + pytest.param( + Null(), + "must be positive", + None, + pytest.raises(ValueError), + id="``expect`` raises ``ValueError`` for ``Null`` and uses the supplied message.", + ), ], ) -def test_expect(monad: Option, msg, expected_result, expected_context) -> None: +def test_expect(request, monad: Option, msg, expected_result, expected_context) -> None: with expected_context: - assert monad.expect(msg) == expected_result + assert example(monad.expect, msg, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "predicate", "expected_result"), [ - pytest.param(Some(3), is_even, Null()), - pytest.param(Some(4), is_even, Some(4)), - pytest.param(Null(), is_even, Null()), + pytest.param( + Some(3), + is_even, + Null(), + id="A ``Some`` that fails the predicate becomes ``Null`` through ``filter_``.", + ), + pytest.param( + Some(4), is_even, Some(4), id="A ``Some`` that passes the predicate remains unchanged." + ), + pytest.param( + Null(), + is_even, + Null(), + id="``filter_`` leaves ``Null`` unchanged without calling the predicate.", + ), ], ) -def test_filter_(monad: Option, predicate, expected_result) -> None: - assert monad.filter_(predicate) == expected_result +def test_filter_(request, monad: Option, predicate, expected_result) -> None: + assert ( + example(monad.filter_, predicate, description=request.node.callspec.id) == expected_result + ) @pytest.mark.parametrize( ("monad", "expected_result"), [ - pytest.param(Some(Some(Some(2))), Some(Some(2))), - pytest.param(Some(Some(2)), Some(2)), - pytest.param(Some(2), Some(2)), - pytest.param(Null(), Null()), + pytest.param( + Some(Some(Some(2))), + Some(Some(2)), + id="Flattening three nested ``Some`` values removes only the outer option.", + ), + pytest.param( + Some(Some(2)), + Some(2), + id="Flattening a doubly wrapped value returns the inner ``Some``.", + ), + pytest.param( + Some(2), Some(2), id="Flattening a ``Some`` with a non-option value does nothing." + ), + pytest.param(Null(), Null(), id="A ``Null`` passes through ``flatten`` unchanged."), ], ) -def test_flatten(monad: Option, expected_result) -> None: - assert monad.flatten() == expected_result +def test_flatten(request, monad: Option, expected_result) -> None: + assert example(monad.flatten, description=request.node.callspec.id) == expected_result def append_to_list(x) -> None: @@ -101,183 +187,396 @@ def append_to_list(x) -> None: @pytest.mark.parametrize( ("monad", "fn", "expected_result"), [ - pytest.param(Some([1]), append_to_list, Some([1])), - pytest.param(Null(), append_to_list, Null()), + pytest.param( + Some([1]), + append_to_list, + Some([1]), + id="Inspecting a ``Some`` calls the function and returns the original option.", + ), + pytest.param( + Null(), + append_to_list, + Null(), + id="Inspecting ``Null`` returns ``Null`` without calling the function.", + ), ], ) -def test_inspect(monad: Option, fn, expected_result) -> None: - assert monad.inspect(fn) == expected_result +def test_inspect(request, monad: Option, fn, expected_result) -> None: + assert example(monad.inspect, fn, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( - ("monad", "expected_result"), [pytest.param(Some(2), False), pytest.param(Null(), True)] + ("monad", "expected_result"), + [ + pytest.param(Some(2), False, id="``is_none`` reports ``False`` for a ``Some``."), + pytest.param(Null(), True, id="For ``Null``, ``is_none`` reports ``True``."), + ], ) -def test_is_none(monad: Option, expected_result) -> None: - assert monad.is_none() == expected_result +def test_is_none(request, monad: Option, expected_result) -> None: + assert example(monad.is_none, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "fn", "expected_result"), [ - pytest.param(Some(1), is_even, False), - pytest.param(Some(2), is_even, True), - pytest.param(Null(), is_even, True), + pytest.param( + Some(1), + is_even, + False, + id="When the predicate rejects a ``Some``, ``is_none_or`` reports ``False``.", + ), + pytest.param( + Some(2), + is_even, + True, + id="When the predicate accepts a ``Some``, ``is_none_or`` reports ``True``.", + ), + pytest.param( + Null(), + is_even, + True, + id="``Null`` makes ``is_none_or`` report ``True`` without calling the predicate.", + ), ], ) -def test_is_none_or(monad: Option, fn, expected_result) -> None: - assert monad.is_none_or(fn) == expected_result +def test_is_none_or(request, monad: Option, fn, expected_result) -> None: + assert example(monad.is_none_or, fn, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( - ("monad", "expected_result"), [pytest.param(Some(2), True), pytest.param(Null(), False)] + ("monad", "expected_result"), + [ + pytest.param(Some(2), True, id="``is_some`` reports ``True`` for a ``Some``."), + pytest.param(Null(), False, id="For ``Null``, ``is_some`` reports ``False``."), + ], ) -def test_is_some(monad: Option, expected_result) -> None: - assert monad.is_some() == expected_result +def test_is_some(request, monad: Option, expected_result) -> None: + assert example(monad.is_some, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "fn", "expected_result"), [ - pytest.param(Some(1), is_even, False), - pytest.param(Some(2), is_even, True), - pytest.param(Null(), is_even, False), + pytest.param( + Some(1), + is_even, + False, + id="When the predicate rejects a ``Some``, ``is_some_and`` reports ``False``.", + ), + pytest.param( + Some(2), + is_even, + True, + id="When the predicate accepts a ``Some``, ``is_some_and`` reports ``True``.", + ), + pytest.param( + Null(), + is_even, + False, + id="A ``Null`` makes ``is_some_and`` report ``False`` without calling the predicate.", + ), ], ) -def test_is_some_and(monad: Option, fn, expected_result) -> None: - assert monad.is_some_and(fn) == expected_result +def test_is_some_and(request, monad: Option, fn, expected_result) -> None: + assert example(monad.is_some_and, fn, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "fn", "expected_result"), - [pytest.param(Some(1), add_one, Some(2)), pytest.param(Null(), add_one, Null())], + [ + pytest.param( + Some(1), + add_one, + Some(2), + id="Mapping a ``Some`` applies the function and wraps its result in a new ``Some``.", + ), + pytest.param( + Null(), + add_one, + Null(), + id="Mapping ``Null`` leaves it unchanged and skips the function.", + ), + ], ) -def test_map(monad: Option, fn, expected_result) -> None: - assert monad.map(fn) == expected_result +def test_map(request, monad: Option, fn, expected_result) -> None: + assert example(monad.map, fn, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "default", "fn", "expected_result"), - [pytest.param(Some("foo"), 42, len, 3), pytest.param(Null(), 42, len, 42)], + [ + pytest.param( + Some("foo"), + 42, + len, + 3, + id="For a ``Some``, ``map_or`` uses the function result instead of the default.", + ), + pytest.param( + Null(), 42, len, 42, id="For ``Null``, ``map_or`` returns the supplied default." + ), + ], ) -def test_map_or(monad: Option, default, fn, expected_result) -> None: - assert monad.map_or(default, fn) == expected_result +def test_map_or(request, monad: Option, default, fn, expected_result) -> None: + assert ( + example(monad.map_or, default, fn, description=request.node.callspec.id) == expected_result + ) @pytest.mark.parametrize( ("monad", "default", "fn", "expected_result"), - [pytest.param(Some("foo"), lambda: 42, len, 3), pytest.param(Null(), lambda: 42, len, 42)], + [ + pytest.param( + Some("foo"), + get_42, + len, + 3, + id="A ``Some`` makes ``map_or_else`` use the mapping function.", + ), + pytest.param( + Null(), get_42, len, 42, id="A ``Null`` makes ``map_or_else`` use the default function." + ), + ], ) -def test_map_or_else(monad: Option, default, fn, expected_result) -> None: - assert monad.map_or_else(default, fn) == expected_result +def test_map_or_else(request, monad: Option, default, fn, expected_result) -> None: + assert ( + example(monad.map_or_else, default, fn, description=request.node.callspec.id) + == expected_result + ) @pytest.mark.parametrize( ("monad", "err", "expected_result"), - [pytest.param(Some("foo"), 0, Ok("foo")), pytest.param(Null(), 0, Err(0))], + [ + pytest.param( + Some("foo"), 0, Ok("foo"), id="A ``Some`` converts to ``Ok`` through ``ok_or``." + ), + pytest.param(Null(), 0, Err(0), id="A ``Null`` converts to ``Err`` through ``ok_or``."), + ], ) -def test_ok_or(monad: Option, err, expected_result) -> None: - assert monad.ok_or(err) == expected_result +def test_ok_or(request, monad: Option, err, expected_result) -> None: + assert example(monad.ok_or, err, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "err", "expected_result"), - [pytest.param(Some("foo"), lambda: 0, Ok("foo")), pytest.param(Null(), lambda: 0, Err(0))], + [ + pytest.param( + Some("foo"), + get_42, + Ok("foo"), + id="A ``Some`` converts to ``Ok`` without calling the error function.", + ), + pytest.param( + Null(), + get_42, + Err(42), + id="A ``Null`` converts to ``Err`` using the error function result.", + ), + ], ) -def test_ok_or_else(monad: Option, err, expected_result) -> None: - assert monad.ok_or_else(err) == expected_result +def test_ok_or_else(request, monad: Option, err, expected_result) -> None: + assert example(monad.ok_or_else, err, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "opt_b", "expected_result"), [ - pytest.param(Some(2), Null(), Some(2)), - pytest.param(Null(), Some(100), Some(100)), - pytest.param(Some(2), Some(100), Some(2)), - pytest.param(Null(), Null(), Null()), + pytest.param( + Some(2), + Null(), + Some(2), + id="A ``Some`` keeps its value when ``or_`` receives ``Null``.", + ), + pytest.param( + Null(), Some(100), Some(100), id="A ``Null`` gives way to a ``Some`` passed to ``or_``." + ), + pytest.param( + Some(2), + Some(100), + Some(2), + id="When both options contain values, ``or_`` keeps the first ``Some``.", + ), + pytest.param( + Null(), Null(), Null(), id="When both options are ``Null``, ``or_`` returns ``Null``." + ), ], ) -def test_or_(monad: Option, opt_b, expected_result) -> None: - assert monad.or_(opt_b) == expected_result +def test_or_(request, monad: Option, opt_b, expected_result) -> None: + assert example(monad.or_, opt_b, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "opt_b", "expected_result"), [ - pytest.param(Some("barbarians"), lambda: Some("vikings"), Some("barbarians")), - pytest.param(Null(), lambda: Some("vikings"), Some("vikings")), - pytest.param(Null(), Null, Null()), + pytest.param( + Some("barbarians"), + get_some_vikings, + Some("barbarians"), + id="A ``Some`` passes through ``or_else`` without calling the function.", + ), + pytest.param( + Null(), + get_some_vikings, + Some("vikings"), + id="For ``Null``, ``or_else`` returns the ``Some`` produced by the function.", + ), + pytest.param( + Null(), + Null, + Null(), + id="If the fallback also produces ``Null``, ``or_else`` returns ``Null``.", + ), ], ) -def test_or_else(monad: Option, opt_b, expected_result) -> None: - assert monad.or_else(opt_b) == expected_result +def test_or_else(request, monad: Option, opt_b, expected_result) -> None: + assert example(monad.or_else, opt_b, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "value", "expected_result"), - [pytest.param(Some(2), 5, Some(5)), pytest.param(Null(), 3, Null())], + [ + pytest.param( + Some(2), + 5, + Some(5), + id="Replacing a ``Some`` returns a new ``Some`` with the replacement value.", + ), + pytest.param(Null(), 3, Null(), id="Replacing ``Null`` leaves it as ``Null``."), + ], ) -def test_replace(monad: Option, value, expected_result) -> None: - assert monad.replace(value) == expected_result +def test_replace(request, monad: Option, value, expected_result) -> None: + assert example(monad.replace, value, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "expected_result", "expected_context"), [ - pytest.param(Some(Ok(2)), Ok(Some(2)), nullcontext()), - pytest.param(Some(Err(2)), Err(Some(2)), nullcontext()), - pytest.param(Null(), Ok(Null()), nullcontext()), - pytest.param(Some(2), None, pytest.raises(TypeError)), + pytest.param( + Some(Ok(2)), + Ok(Some(2)), + nullcontext(), + id="Transposing ``Some(Ok(value))`` produces ``Ok(Some(value))``.", + ), + pytest.param( + Some(Err(2)), + Err(Some(2)), + nullcontext(), + id="Transposing ``Some(Err(error))`` produces ``Err(Some(error))``.", + ), + pytest.param( + Null(), Ok(Null()), nullcontext(), id="Transposing ``Null`` produces ``Ok(Null())``." + ), + pytest.param( + Some(2), + None, + pytest.raises(TypeError), + id="Transposing a ``Some`` with an unsupported value raises ``TypeError``.", + ), ], ) -def test_transpose(monad: Option, expected_result, expected_context) -> None: +def test_transpose(request, monad: Option, expected_result, expected_context) -> None: with expected_context: - assert monad.transpose() == expected_result + assert example(monad.transpose, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "expected_result", "expected_context"), - [pytest.param(Some(2), 2, nullcontext()), pytest.param(Null(), None, pytest.raises(TypeError))], + [ + pytest.param( + Some(2), 2, nullcontext(), id="``unwrap`` extracts the value from a ``Some``." + ), + pytest.param( + Null(), + None, + pytest.raises(TypeError), + id="``unwrap`` raises ``TypeError`` when the option is ``Null``.", + ), + ], ) -def test_unwrap(monad: Option, expected_result, expected_context) -> None: +def test_unwrap(request, monad: Option, expected_result, expected_context) -> None: with expected_context: - assert monad.unwrap() == expected_result + assert example(monad.unwrap, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "default", "expected_result"), - [pytest.param(Some("car"), "bike", "car"), pytest.param(Null(), "bike", "bike")], + [ + pytest.param( + Some("car"), + "bike", + "car", + id="With a ``Some``, ``unwrap_or`` returns the value and ignores the default.", + ), + pytest.param( + Null(), "bike", "bike", id="With ``Null``, ``unwrap_or`` returns the default." + ), + ], ) -def test_unwrap_or(monad: Option, default, expected_result) -> None: - assert monad.unwrap_or(default) == expected_result +def test_unwrap_or(request, monad: Option, default, expected_result) -> None: + assert ( + example(monad.unwrap_or, default, description=request.node.callspec.id) == expected_result + ) @pytest.mark.parametrize( ("monad", "fn", "expected_result"), - [pytest.param(Some(4), lambda: 20, 4), pytest.param(Null(), lambda: 20, 20)], + [ + pytest.param( + Some(4), + get_42, + 4, + id="A ``Some`` makes ``unwrap_or_else`` return its value without calling the function.", + ), + pytest.param( + Null(), get_42, 42, id="A ``Null`` makes ``unwrap_or_else`` return the function result." + ), + ], ) -def test_unwrap_or_else(monad: Option, fn, expected_result) -> None: - assert monad.unwrap_or_else(fn) == expected_result +def test_unwrap_or_else(request, monad: Option, fn, expected_result) -> None: + assert ( + example(monad.unwrap_or_else, fn, description=request.node.callspec.id) == expected_result + ) @pytest.mark.parametrize( ("monad", "other", "expected_result"), [ - pytest.param(Some(1), Some("hi"), Some((1, "hi"))), - pytest.param(Some(1), Null(), Null()), - pytest.param(Null(), Some(1), Null()), + pytest.param( + Some(1), + Some("hi"), + Some((1, "hi")), + id="Zipping two ``Some`` values produces a ``Some`` containing both values.", + ), + pytest.param( + Some(1), Null(), Null(), id="Zipping a ``Some`` with ``Null`` produces ``Null``." + ), + pytest.param( + Null(), Some(1), Null(), id="Zipping ``Null`` with a ``Some`` produces ``Null``." + ), ], ) -def test_zip(monad: Option, other, expected_result) -> None: - assert monad.zip(other) == expected_result +def test_zip(request, monad: Option, other, expected_result) -> None: + assert example(monad.zip, other, description=request.node.callspec.id) == expected_result @pytest.mark.parametrize( ("monad", "expected_result"), [ - pytest.param(Some((2, 2)), (Some(2), Some(2))), - pytest.param(Some(4), (Null(), Null())), - pytest.param(Null(), (Null(), Null())), + pytest.param( + Some((2, 2)), + (Some(2), Some(2)), + id="Unzipping a ``Some`` pair produces two ``Some`` values.", + ), + pytest.param( + Some(4), + (Null(), Null()), + id="Unzipping a ``Some`` with a non-pair value produces two ``Null`` values.", + ), + pytest.param( + Null(), (Null(), Null()), id="Unzipping ``Null`` produces two ``Null`` values." + ), ], ) -def test_unzip(monad: Option, expected_result) -> None: - assert monad.unzip() == expected_result +def test_unzip(request, monad: Option, expected_result) -> None: + assert example(monad.unzip, description=request.node.callspec.id) == expected_result diff --git a/tests/monads/test_result.py b/tests/monads/test_result.py deleted file mode 100644 index 01a053a..0000000 --- a/tests/monads/test_result.py +++ /dev/null @@ -1,135 +0,0 @@ -from contextlib import nullcontext - -import pytest - -from danom import Err, Ok, Result -from tests.conftest import add_one - - -@pytest.mark.parametrize( - ("monad", "inner"), - [pytest.param(Ok, 0), pytest.param(Ok, "ok"), pytest.param(Err, 0), pytest.param(Result, 0)], -) -def test_unit(monad, inner): - assert monad.unit(inner) == Ok(inner) - - -@pytest.mark.parametrize( - ("left", "right", "expected_result"), - [ - pytest.param(Ok(), Err(), False), - pytest.param(Ok(), Ok(), True), - pytest.param(Err(), Err(), True), - ], -) -def test_result_equality(left, right, expected_result): - assert (left == right) == expected_result - - -@pytest.mark.parametrize( - ("monad", "expected_result", "expected_context"), - [pytest.param(Result, None, pytest.raises(TypeError))], -) -def test_result_unwrap(monad, expected_result, expected_context): - with expected_context: - assert monad().unwrap() == expected_result - - -@pytest.mark.parametrize( - ("monad", "expected_result", "expected_context"), - [ - pytest.param(Ok(None), None, nullcontext()), - pytest.param(Ok(0), 0, nullcontext()), - pytest.param(Ok("ok"), "ok", nullcontext()), - pytest.param(Err(error=TypeError("should raise this")), None, pytest.raises(TypeError)), - pytest.param(Err(error=ValueError("should raise this")), None, pytest.raises(ValueError)), - pytest.param(Err("some other err representation"), None, pytest.raises(ValueError)), - ], -) -def test_unwrap(monad, expected_result, expected_context): - with expected_context: - assert monad.unwrap() == expected_result - - -@pytest.mark.parametrize( - ("monad", "expected_result"), [pytest.param(Ok(None), True), pytest.param(Err(), False)] -) -def test_is_ok(monad, expected_result): - assert monad.is_ok() == expected_result - - -@pytest.mark.parametrize( - ("monad", "func", "expected_result"), - [pytest.param(Ok(0), add_one, Ok(1)), pytest.param(Err(), add_one, Err())], -) -def test_map(monad, func, expected_result): - assert monad.map(func) == expected_result - - -@pytest.mark.parametrize( - ("monad", "func", "expected_result"), - [pytest.param(Ok(0), add_one, Ok(0)), pytest.param(Err(0), add_one, Err(1))], -) -def test_map_err(monad, func, expected_result): - assert monad.map_err(func) == expected_result - - -class OnlyIsOk(Result): - def is_ok(self) -> bool: - return False - - -class OnlyUnwrap(Result): - def unwrap(self) -> None: - return None - - -@pytest.mark.parametrize("cls", [pytest.param(OnlyIsOk), pytest.param(OnlyUnwrap)]) -def test_raises_not_implemented(cls): - with pytest.raises(TypeError): - cls() - - -@pytest.mark.parametrize( - ("err", "expected_details"), - [pytest.param(TypeError("an invalid type"), []), pytest.param("A primative err", [])], -) -def test_err_details(err, expected_details): - monad = Err(error=err) - assert monad.details == expected_details - - -@pytest.mark.parametrize( - ("monad", "expected_result"), [pytest.param(Ok(None), True), pytest.param(Err(), False)] -) -def test_staticmethod_result_is_ok(monad, expected_result): - assert Result.result_is_ok(monad) == expected_result - - -@pytest.mark.parametrize( - ("monad", "expected_result", "expected_context"), - [ - pytest.param(Ok(None), None, nullcontext()), - pytest.param(Ok(0), 0, nullcontext()), - pytest.param(Ok("ok"), "ok", nullcontext()), - pytest.param(Err(error=TypeError("should raise this")), None, pytest.raises(TypeError)), - pytest.param(Err(error=ValueError("should raise this")), None, pytest.raises(ValueError)), - pytest.param(Err("some other err representation"), None, pytest.raises(ValueError)), - ], -) -def test_staticmethod_result_unwrap(monad, expected_result, expected_context): - with expected_context: - assert Result.result_unwrap(monad) == expected_result - - -@pytest.mark.parametrize( - ("monad", "expected_result"), - [ - pytest.param(Ok(Ok()), Ok()), - pytest.param(Ok(Err()), Err()), - pytest.param(Err(Ok()), Ok()), - pytest.param(Err(Err()), Err()), - ], -) -def test_flatten(monad, expected_result) -> None: - assert monad.flatten() == expected_result diff --git a/tests/monads/test_result_v2.py b/tests/monads/test_result_v2.py new file mode 100644 index 0000000..4492809 --- /dev/null +++ b/tests/monads/test_result_v2.py @@ -0,0 +1,558 @@ +from contextlib import nullcontext +from typing import cast + +import pytest + +from danom._monads._option import Null, Some +from danom._monads._result_v2 import Err, Ok, Result +from dev_tools.create_examples.collection.example import example +from tests.conftest import add_one, get_42, get_ok_vikings, is_even + + +@pytest.mark.parametrize( + ("monad", "res", "expected_result"), + [ + pytest.param( + Ok(2), + Err("late error"), + Err("late error"), + id="If you call ``and_`` on an ``Ok`` with an ``Err`` as the arg then the ``Err`` takes precedence over the ``Ok``.", + ), + pytest.param( + Err("early error"), + Ok("foo"), + Err("early error"), + id="If you call ``and_`` on an ``Err`` with an ``Ok`` as the arg then the ``Err`` still takes precedence over the ``Ok``.", + ), + pytest.param( + Err("not a 2"), + Err("late error"), + Err("not a 2"), + id="If you call ``and_`` on an ``Err`` with an ``Err`` as the arg then the first ``Err`` is returned and the second is discarded.", + ), + pytest.param( + Ok(2), + Ok("different result type"), + Ok("different result type"), + id="Whereas, calling ``and_`` on an ``Ok`` with another ``Ok`` as the arg then the first ``Ok`` is discarded and the second is returned.", + ), + ], +) +def test_and_(request, monad: Result, res, expected_result) -> None: + assert example(monad.and_, res, description=request.node.callspec.id) == expected_result + + +def must_be_less_than_10(x: int) -> Result[int, str]: + return cast(Result[int, str], Ok(x) if x < 10 else Err("too high")) + + +@pytest.mark.parametrize( + ("monad", "fn", "expected_result"), + [ + pytest.param( + Ok(2), + must_be_less_than_10, + Ok(2), + id="An ``Ok`` passed to ``and_then`` returns the ``Ok`` produced by the function.", + ), + pytest.param( + Ok(20), + must_be_less_than_10, + Err("too high"), + id="When the function passed to ``and_then`` produces an ``Err``, that ``Err`` becomes the result.", + ), + pytest.param( + Err("not a number"), + must_be_less_than_10, + Err("not a number"), + id="An existing ``Err`` passes through ``and_then`` unchanged, without calling the function.", + ), + ], +) +def test_and_then(request, monad: Result, fn, expected_result) -> None: + assert example(monad.and_then, fn, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "expected_result"), + [ + pytest.param( + Ok(2), Ok(2), id="Cloning an ``Ok`` creates a separate ``Ok`` with the same value." + ), + pytest.param( + Err(2), Err(2), id="Cloning an ``Err`` creates a separate ``Err`` with the same value." + ), + ], +) +def test_cloned(request, monad: Result, expected_result) -> None: + assert example(monad.cloned, description=request.node.callspec.id) == expected_result + assert id(monad) != id(expected_result) + + +@pytest.mark.parametrize( + ("monad", "expected_result"), + [ + pytest.param(Ok(2), Null(), id="An ``Ok`` has no error, so ``err`` returns ``Null``."), + pytest.param( + Err("Nothing here"), + Some("Nothing here"), + id="If you call ``err`` on an ``Err`` then ``Some`` with the wrapped error is returned.", + ), + ], +) +def test_err(request, monad: Result, expected_result) -> None: + assert example(monad.err, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "msg", "expected_result", "expected_context"), + [ + pytest.param( + Ok(2), + "must be positive", + 2, + nullcontext(), + id="``expect`` extracts the value from an ``Ok``.", + ), + pytest.param( + Err(), + "must be positive", + None, + pytest.raises(ValueError), + id="``expect`` raises ``ValueError`` for an ``Err`` and uses the supplied message.", + ), + ], +) +def test_expect(request, monad: Result, msg, expected_result, expected_context) -> None: + with expected_context: + assert example(monad.expect, msg, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "msg", "expected_result", "expected_context"), + [ + pytest.param( + Err(2), + "must be err", + 2, + nullcontext(), + id="``expect_err`` extracts the error from an ``Err``.", + ), + pytest.param( + Ok(), + "must be err", + None, + pytest.raises(ValueError), + id="``expect_err`` raises ``ValueError`` for an ``Ok`` and uses the supplied message.", + ), + ], +) +def test_expect_err(request, monad: Result, msg, expected_result, expected_context) -> None: + with expected_context: + assert ( + example(monad.expect_err, msg, description=request.node.callspec.id) == expected_result + ) + + +@pytest.mark.parametrize( + ("monad", "expected_result"), + [ + pytest.param( + Ok(Ok(Ok(2))), + Ok(Ok(2)), + id="If you call ``flatten`` on three nested ``Ok`` values then two nested ``Ok`` values are returned. Only the outer monad is removed", + ), + pytest.param( + Ok(Ok(2)), + Ok(2), + id="This is made obvious calling ``flatten`` on a doubly wrapped value returning just the inner monad.", + ), + pytest.param( + Ok(2), + Ok(2), + id="Calling ``flatten`` if the inner is not a monad of the same type is a no-op.", + ), + pytest.param(Err(), Err(), id="An ``Err`` passes through ``flatten`` unchanged."), + ], +) +def test_flatten(request, monad: Result, expected_result) -> None: + assert example(monad.flatten, description=request.node.callspec.id) == expected_result + + +def append_to_list(x) -> None: + x.append(2) + + +@pytest.mark.parametrize( + ("monad", "fn", "expected_result"), + [ + pytest.param( + Ok([1]), + append_to_list, + Ok([1]), + id="If you call ``inspect`` on an ``Ok`` then the original ``Ok`` is returned after the function is called, this is used for logging or side outputs that shouldn't disrupt the existing flow.", + ), + pytest.param( + Err("un-appendable"), + append_to_list, + Err("un-appendable"), + id="If you call ``inspect`` on an ``Err`` then the ``Err`` is returned and the function is not called.", + ), + ], +) +def test_inspect(request, monad: Result, fn, expected_result) -> None: + assert example(monad.inspect, fn, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "fn", "expected_result"), + [ + pytest.param( + Err([1]), + append_to_list, + Err([1]), + id="If you call ``inspect_err`` on an ``Err`` then the original ``Err`` is returned after the function is called, it's essentially the inverse of ``inspect``.", + ), + pytest.param( + Ok("un-appendable"), + append_to_list, + Ok("un-appendable"), + id="Similarly to calling ``inspect`` on an ``Err``, if you call ``inspect_err`` on an ``Ok`` then the ``Ok`` is returned and the function is not called.", + ), + ], +) +def test_inspect_err(request, monad: Result, fn, expected_result) -> None: + assert example(monad.inspect_err, fn, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "expected_result"), + [ + pytest.param(Ok(2), False, id="``is_err`` reports ``False`` for an ``Ok``."), + pytest.param(Err(2), True, id="For an ``Err``, ``is_err`` reports ``True``."), + ], +) +def test_is_err(request, monad: Result, expected_result) -> None: + assert example(monad.is_err, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "fn", "expected_result"), + [ + pytest.param( + Err(1), + is_even, + False, + id="``is_err_and`` requires both the monad to be an ``Err`` and the returned value of the passed in callable to be ``True``, for example, if you called ``is_err_and`` with a function that checks for evenness on an ``Err(1)`` then the result is ``False``.", + ), + pytest.param( + Err(2), + is_even, + True, + id="Therefore, if you call ``is_err_and`` on an ``Err`` with a function that returns ``True`` then ``True`` is returned.", + ), + pytest.param( + Ok(2), + is_even, + False, + id="Whereas, if you call ``is_err_and`` on an ``Ok`` with a function then ``False`` is returned and the function is not called.", + ), + ], +) +def test_is_err_and(request, monad: Result, fn, expected_result) -> None: + assert example(monad.is_err_and, fn, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "expected_result"), + [ + pytest.param(Ok(2), True, id="``is_ok`` reports ``True`` for an ``Ok``."), + pytest.param(Err(2), False, id="For an ``Err``, ``is_ok`` reports ``False``."), + ], +) +def test_is_ok(request, monad: Result, expected_result) -> None: + assert example(monad.is_ok, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "fn", "expected_result"), + [ + pytest.param( + Ok(1), + is_even, + False, + id="When the predicate rejects an ``Ok``, ``is_ok_and`` reports ``False``.", + ), + pytest.param( + Ok(2), + is_even, + True, + id="When the predicate accepts an ``Ok``, ``is_ok_and`` reports ``True``.", + ), + pytest.param( + Err(2), + is_even, + False, + id="An ``Err`` makes ``is_ok_and`` report ``False`` without calling the predicate.", + ), + ], +) +def test_is_ok_and(request, monad: Result, fn, expected_result) -> None: + assert example(monad.is_ok_and, fn, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "fn", "expected_result"), + [ + pytest.param( + Ok(1), + add_one, + Ok(2), + id="Mapping an ``Ok`` applies the function and wraps its result in a new ``Ok``.", + ), + pytest.param( + Err(1), + add_one, + Err(1), + id="Mapping an ``Err`` leaves it unchanged and skips the function.", + ), + ], +) +def test_map(request, monad: Result, fn, expected_result) -> None: + assert example(monad.map, fn, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "fn", "expected_result"), + [ + pytest.param( + Err(1), + add_one, + Err(2), + id="Mapping an ``Err`` applies the function and wraps its result in a new ``Err``.", + ), + pytest.param( + Ok(1), + add_one, + Ok(1), + id="Mapping an ``Ok`` leaves it unchanged and skips the function.", + ), + ], +) +def test_map_err(request, monad: Result, fn, expected_result) -> None: + assert example(monad.map_err, fn, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "default", "fn", "expected_result"), + [ + pytest.param( + Ok("foo"), + 42, + len, + 3, + id="For an ``Ok``, ``map_or`` uses the function result instead of the default.", + ), + pytest.param( + Err(), 42, len, 42, id="For an ``Err``, ``map_or`` returns the supplied default." + ), + ], +) +def test_map_or(request, monad: Result, default, fn, expected_result) -> None: + assert ( + example(monad.map_or, default, fn, description=request.node.callspec.id) == expected_result + ) + + +@pytest.mark.parametrize( + ("monad", "default", "fn", "expected_result"), + [ + pytest.param( + Ok("foo"), + get_42, + len, + 3, + id="An ``Ok`` makes ``map_or_else`` use the mapping function.", + ), + pytest.param( + Err(), get_42, len, 42, id="An ``Err`` makes ``map_or_else`` use the default function." + ), + ], +) +def test_map_or_else(request, monad: Result, default, fn, expected_result) -> None: + assert ( + example(monad.map_or_else, default, fn, description=request.node.callspec.id) + == expected_result + ) + + +@pytest.mark.parametrize( + ("monad", "expected_result"), + [ + pytest.param(Ok(2), Some(2), id="An ``Ok`` converts to ``Some`` through ``ok``."), + pytest.param(Err(2), Null(), id="An ``Err`` converts to ``Null`` through ``ok``."), + ], +) +def test_ok(request, monad: Result, expected_result) -> None: + assert example(monad.ok, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "res", "expected_result"), + [ + pytest.param( + Ok(2), + Err("foo"), + Ok(2), + id="An ``Ok`` keeps its value when ``or_`` receives an ``Err``.", + ), + pytest.param( + Err("foo"), Ok(100), Ok(100), id="An ``Err`` gives way to an ``Ok`` passed to ``or_``." + ), + pytest.param( + Ok(2), Ok(100), Ok(2), id="When both values are ``Ok``, ``or_`` keeps the first one." + ), + pytest.param( + Err("foo"), + Err("foo"), + Err("foo"), + id="When both values are ``Err``, ``or_`` keeps the first error.", + ), + ], +) +def test_or_(request, monad: Result, res, expected_result) -> None: + assert example(monad.or_, res, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "fn", "expected_result"), + [ + pytest.param( + Ok("barbarians"), + get_ok_vikings, + Ok("barbarians"), + id="An ``Ok`` passes through ``or_else`` without calling the function.", + ), + pytest.param( + Err("foo"), + get_ok_vikings, + Ok("vikings"), + id="For an ``Err``, ``or_else`` returns the ``Ok`` produced by the function.", + ), + pytest.param( + Err("foo"), + Err, + Err("foo"), + id="If the fallback also produces an ``Err``, ``or_else`` keeps the original error.", + ), + ], +) +def test_or_else(request, monad: Result, fn, expected_result) -> None: + assert example(monad.or_else, fn, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "expected_result", "expected_context"), + [ + pytest.param( + Ok(Some(5)), + Some(Ok(5)), + nullcontext(), + id="Transposing ``Ok(Some(value))`` produces ``Some(Ok(value))``.", + ), + pytest.param( + Ok(Null()), Null(), nullcontext(), id="Transposing ``Ok(Null())`` produces ``Null``." + ), + pytest.param( + Err(), Some(Err()), nullcontext(), id="Transposing an ``Err`` wraps it in ``Some``." + ), + pytest.param( + Ok(2), + None, + pytest.raises(TypeError), + id="Transposing an ``Ok`` with an unsupported value raises ``TypeError``.", + ), + ], +) +def test_transpose(request, monad: Result, expected_result, expected_context) -> None: + with expected_context: + assert example(monad.transpose, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "expected_result", "expected_context"), + [ + pytest.param(Ok(2), 2, nullcontext(), id="``unwrap`` extracts the value from an ``Ok``."), + pytest.param( + Err(), + None, + pytest.raises(TypeError), + id="``unwrap`` raises ``TypeError`` when the result is an ``Err``.", + ), + ], +) +def test_unwrap(request, monad: Result, expected_result, expected_context) -> None: + with expected_context: + assert example(monad.unwrap, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "expected_result", "expected_context"), + [ + pytest.param( + Err("failed"), + "failed", + nullcontext(), + id="``unwrap_err`` extracts the error from an ``Err``.", + ), + pytest.param( + Ok(2), + None, + pytest.raises(TypeError), + id="``unwrap_err`` raises ``TypeError`` when the result is an ``Ok``.", + ), + ], +) +def test_unwrap_err(request, monad: Result, expected_result, expected_context) -> None: + with expected_context: + assert example(monad.unwrap_err, description=request.node.callspec.id) == expected_result + + +@pytest.mark.parametrize( + ("monad", "default", "expected_result"), + [ + pytest.param( + Ok("car"), + "bike", + "car", + id="With an ``Ok``, ``unwrap_or`` returns the value and ignores the default.", + ), + pytest.param( + Err(), "bike", "bike", id="With an ``Err``, ``unwrap_or`` returns the default." + ), + ], +) +def test_unwrap_or(request, monad: Result, default, expected_result) -> None: + assert ( + example(monad.unwrap_or, default, description=request.node.callspec.id) == expected_result + ) + + +@pytest.mark.parametrize( + ("monad", "fn", "expected_result"), + [ + pytest.param( + Ok(4), + get_42, + 4, + id="An ``Ok`` makes ``unwrap_or_else`` return its value without calling the function.", + ), + pytest.param( + Err(), get_42, 42, id="An ``Err`` makes ``unwrap_or_else`` return the function result." + ), + ], +) +def test_unwrap_or_else(request, monad: Result, fn, expected_result) -> None: + assert ( + example(monad.unwrap_or_else, fn, description=request.node.callspec.id) == expected_result + ) diff --git a/tests/test_safe.py b/tests/test_safe.py index 8923577..a379d7a 100644 --- a/tests/test_safe.py +++ b/tests/test_safe.py @@ -68,7 +68,6 @@ def test_traceback(): "Traceback (most recent call last):", ' File "./src/danom/_monads/_safe.py", line 68, in wrapper', " return Ok(func(*args, **kwargs))", - ' File "./tests/conftest.py", line 124, in div_zero', " return x / 0", "ZeroDivisionError: division by zero", ] @@ -76,7 +75,10 @@ def test_traceback(): if not isinstance(err, Err): raise TypeError("This should be an Err by now") - tb_lines = err.traceback.replace(str(REPO_ROOT), ".").splitlines() + tb_lines = [ + line.split("#")[0].rstrip() + for line in err.traceback.replace(str(REPO_ROOT), ".").splitlines() + ] missing_lines = [line for line in expected_lines if line not in tb_lines] diff --git a/uv.lock b/uv.lock index a472c94..c4ee7c4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,11 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] [options] exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. @@ -357,6 +362,7 @@ dependencies = [ dev = [ { name = "hypothesis" }, { name = "ipykernel" }, + { name = "libcst" }, { name = "papertrail" }, { name = "pre-commit" }, { name = "pytest" }, @@ -376,6 +382,7 @@ requires-dist = [{ name = "attrs", specifier = ">=25.4.0" }] dev = [ { name = "hypothesis", specifier = ">=6.148.2" }, { name = "ipykernel", specifier = ">=7.1.0" }, + { name = "libcst", specifier = ">=1.9.0" }, { name = "papertrail", specifier = ">=0.1.2" }, { name = "pre-commit", specifier = ">=4.5.0" }, { name = "pytest", specifier = ">=9.0.1" }, @@ -624,6 +631,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] +[[package]] +name = "libcst" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml", marker = "python_full_version != '3.13.*'" }, + { name = "pyyaml-ft", marker = "python_full_version == '3.13.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/c0/098e5c91ff1537f00c85a6438b6cb1863d17144680cc91f47c87f104a200/libcst-1.9.0.tar.gz", hash = "sha256:087b58a9afe076bb08e2d726478e1f16cb928d67ffa9092817e033c335de522a", size = 914739, upload-time = "2026-07-29T21:28:43.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/bb/d22c37c33dfe18084634f5ef89f8f0749ffe7b6e0ad312722aafd86bbbdb/libcst-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd1a3500c41784075c4946a995d5ad89f68fa0d226b63ff3c4d78f6ea6dd23e5", size = 2043159, upload-time = "2026-07-29T19:24:49.013Z" }, + { url = "https://files.pythonhosted.org/packages/10/b8/2dedef84d72e7271119217503b69ed6dc5d0b2077685e163caae669d9c70/libcst-1.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:611cebd3bbc2014576f4dcc7b845b3c594c96ddc287a3db9b78f22eff156a7d3", size = 2203245, upload-time = "2026-07-29T19:24:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/e8/90/e02ac2dad647423f947bb11f8322bfdba8ccfdd380e6c7b695add2d1acd4/libcst-1.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8d731abe1307720ea1a52d447555e8443a6d130e0e520243c0634a58f6edbc9d", size = 2255388, upload-time = "2026-07-29T19:24:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/13/5f/6089a51518cfd2ff40950eb26bcc36951d7b6d4f4213568aa0290265aff7/libcst-1.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5351d92ca6ac1cc32e097700e1161ad1ceaa4d9b2cca5abadb1e94576b325", size = 2268982, upload-time = "2026-07-29T19:24:53.562Z" }, + { url = "https://files.pythonhosted.org/packages/ed/78/26881ec466fb70cbc129dca26ccb5a52a0061face2c5822e4b61f00f9699/libcst-1.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03165a264653bb77f6a11b412ae09c08bdb0c25864f3b8d42b816ac64b9d4b9e", size = 2378174, upload-time = "2026-07-29T19:24:55.175Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7a/a4dba5f11faf12a12ffba06d18019987851aab33b590242a602ffd4fb1bd/libcst-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:b755ed4a4bc2faee849b54820023137d60d4c199e0a0822f0ff0c1bc49e49b48", size = 2103462, upload-time = "2026-07-29T19:24:56.966Z" }, + { url = "https://files.pythonhosted.org/packages/f5/13/57cb129093e0d6744b3c914ba6cbbdf51ed1b2c823882936c553a3a0cf1b/libcst-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e50576bad7d56459d9792cd0b0dfe5469dad13646e9a9c0a8b1ba20b269f332", size = 1979825, upload-time = "2026-07-29T19:24:58.403Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b1/befc0544283bb3923a928accf79ed685e5a725524bdb3491826670affc07/libcst-1.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8b9df30f524317b097dc53065b25dda33d6a4cc3c7c8bf4fc83ca7559c58cc0", size = 2043754, upload-time = "2026-07-29T19:24:59.885Z" }, + { url = "https://files.pythonhosted.org/packages/45/50/fef7c172a8457c95894edf5fb04805024899cdfea41fa01b0636587b79e1/libcst-1.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e465a7bc9c2b9533eb9e06d2391f8819f811b5112919d4064c9fb8565aaafa08", size = 2203548, upload-time = "2026-07-29T19:25:01.323Z" }, + { url = "https://files.pythonhosted.org/packages/18/ff/764cd2be1fd99d774fc44039c319dc0ed1d9d9afeaa02759a05121cccd4b/libcst-1.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8504b422c95676a8c27b517e1ac01413ece91bf356865c587ca9bdcd5708a2f7", size = 2255274, upload-time = "2026-07-29T19:25:02.764Z" }, + { url = "https://files.pythonhosted.org/packages/34/a7/474748a27a02fa83e3556b260d5f5236ca48164fa7beffecf3b2cad24dca/libcst-1.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bcb9f9d4fcfe2ec7a40d2c26e03a538d5d9dc189c38551eb3f94ab661afee7c0", size = 2269126, upload-time = "2026-07-29T19:25:04.158Z" }, + { url = "https://files.pythonhosted.org/packages/8a/b7/655e45363b8cf87b91e41e060b21c89e5316c7ef36eb14a1a498b27bb71e/libcst-1.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8d671c39a431c309476099b8ec811e412503ec0f4465f6fc907cb51c70e8e6b", size = 2378602, upload-time = "2026-07-29T19:25:06.424Z" }, + { url = "https://files.pythonhosted.org/packages/db/13/6da63f0902ece43bf9d737017251ad7ad06edd9d3c4e1856450403cb4473/libcst-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:4d382fba04077eb556a1ea4295a4482e192aa93639c5a07ba0885841965ea0c0", size = 2103486, upload-time = "2026-07-29T19:25:07.979Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ff/dccb1a55e38b4e47256a97242d61d2bef5366c744faf88fb087d4fa0f995/libcst-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:a621e261990148c1cfbe26c1798bd2375c131cf358a44a7a4659fc41c1a336e1", size = 1979907, upload-time = "2026-07-29T19:25:09.532Z" }, + { url = "https://files.pythonhosted.org/packages/65/2a/4943c71d90975bc59034a057dea346b365c276f308c1d31f5cf2bd85492d/libcst-1.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eccf4c57d273cdd3fe1c67b72cf9bb1bbd4547aa011824e96ccf5b7136057aa4", size = 2044529, upload-time = "2026-07-29T19:25:11.04Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1168b98c2a0f338be3a44753647baab051feaf6167cc887bf4447f8fd920/libcst-1.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:32395244edfe6538e0ea2bf82051d60103d3f54861274805c4fb3745efb70a85", size = 2203519, upload-time = "2026-07-29T19:25:12.543Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/2eaa697a80f899bcf2245680bbfcc1e478eb3b3328c21d4b2880bdf00dac/libcst-1.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:444e84c76cd035cd2fe136838c1a524d34b08216521f6f5093df8a6f6cfa5799", size = 2255879, upload-time = "2026-07-29T19:25:14.137Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/793b9fd96dd52d202f5f2b88d7e54f05ebed767d1bb3b32395a1817bf6ab/libcst-1.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bb5d0946f2b4c6711b5d69fe4f833b364f9e7a1a2b08f88b98619dc18975099a", size = 2269807, upload-time = "2026-07-29T19:25:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/a3/f4/1bc7aaea03971c45e8a885fed9fc1c73176dbbd00b0296a3cde9529bafd6/libcst-1.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:45808c03528b3ad40b14095348a918e08d98c47b4a125d631cb78e817c0a5b16", size = 2378171, upload-time = "2026-07-29T19:25:17.289Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f7/bc49e367d2bc8817213594dd52cf6b2da2dbbda90b5382d60653999274ac/libcst-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:568288cdbfe3b4ca3ae4852cb0a439ff053dd54c841bc4995bf2b71238b5de40", size = 2177847, upload-time = "2026-07-29T19:25:19.35Z" }, + { url = "https://files.pythonhosted.org/packages/87/80/4d81577a22e6d535d1a3409f3a1c6903e09036f0e17fc47f92169dbc3501/libcst-1.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:107593af46945593e7825821793393262bc4fa1d3ea24c3ed487b61269bbbdf8", size = 2058134, upload-time = "2026-07-29T19:25:20.769Z" }, + { url = "https://files.pythonhosted.org/packages/d1/7f/c3f3a0e7a1a2adaa76e815e82a7814d6bfe7d49432276bba652248c68d0b/libcst-1.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f6248cb07444ab9a6733a855737a9febed8b9adca51347019348b09a3ac7dfe9", size = 2036102, upload-time = "2026-07-29T19:25:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/4e/af/2f5543255b2c7d749b966adeccc6bfb1652cf8b425afb913d0f3f025a865/libcst-1.9.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:496c24e0d3240bc7da45dae543aff3f5b6509978c39262d6b84ed2fb999dded2", size = 2194806, upload-time = "2026-07-29T19:25:24.017Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5b/03f4cddce426d005e208b39ea7b2d456e667cdee0f1891360f0fc8430f20/libcst-1.9.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ea490fa8540503db5f321f0268becab46eb50f710e8cec8041e241fd66f6874f", size = 2246762, upload-time = "2026-07-29T19:25:25.365Z" }, + { url = "https://files.pythonhosted.org/packages/d8/31/9d5fe1e43dc3dbcc74f70f3e0e73fcdd8d84effc1059d8b45974f0d0d2eb/libcst-1.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50ab94bb2524b419056d4003032b8c67102ac800f8d85b3c4260a01746d94dbb", size = 2259633, upload-time = "2026-07-29T19:25:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2b/6752b28d88c3a19b3bc0b9e1838443b6760d5c862b4f4b37955402659e24/libcst-1.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a2faaf92500d0226358125630f5aab4758e8aad3f2d70a10892ec3c700781a54", size = 2368043, upload-time = "2026-07-29T19:25:28.344Z" }, + { url = "https://files.pythonhosted.org/packages/e2/94/775825b2637f8ab05694b6a4b3802ae6783b4e799f9b58d2400c7e2d4369/libcst-1.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c7b548512db25af9c2997a95fa731bd6b6928ecbad6c0915d7482d8bb42d34f", size = 2176432, upload-time = "2026-07-29T19:25:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/88ad67427c6fd9db929e087912b0a540e5140e5cb77e7ca4170edaac8531/libcst-1.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:497d5329345f1f5df84e41b0bbd00204b64a2fd30dfe3cfaeaebca633a31e877", size = 2052931, upload-time = "2026-07-29T19:25:31.397Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/ad790b043a38b10cea13166c8dd716654ad8b227c20f12eebf6326191cd9/libcst-1.9.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:a5068bf6114f6f4d79af7a6c80a28d1deb50441150ea1db13b5458ab34bec159", size = 2043987, upload-time = "2026-08-11T05:54:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/15/6b/d3cd8275cc54ffc9817ade91442b3e6e9edd1a4518bd4e6dda13e3152dbe/libcst-1.9.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:7acfd18adcdd32dcf41ce676cf121002afcbe9ad2c72dc0c18d78465beedc228", size = 2204003, upload-time = "2026-08-11T05:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/da/62/87eddccb11d5d221d50ac4fff4b6e9b8d48c547028d01f7d0fbc5c8a1d75/libcst-1.9.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:86361e2b426bd1b403e52375703c8b764e226e571adcb30442510710681edbf0", size = 2256053, upload-time = "2026-08-11T05:54:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/a44126aeb9fca8b4e342e0cc803ea00ca8f6cd5712619a7897b3eba13797/libcst-1.9.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8c14abe844bec8b021111b3985474e49f5af799c020e8e40dedcac87c97a40c5", size = 2270576, upload-time = "2026-08-11T05:54:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/bb/51/3af3dd44b117d66486e0ed61e43a16b7fc8cc5c372e1dd4ca0e70b888d46/libcst-1.9.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:8930971d2299b7006bb5d10c10038f44efb6da89d5bca2823595e31a9cdb96af", size = 2378573, upload-time = "2026-08-11T05:54:10.093Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e8/e545087d298dbf6494e84a8092f0f5dbd62eedacabd4ecd6b364367b4804/libcst-1.9.0-cp315-cp315-win_amd64.whl", hash = "sha256:5891ce9cff815077614f509e3a23888c5ddb8081d6188e8ba1172c0ccc369022", size = 2177937, upload-time = "2026-08-11T05:54:12.168Z" }, + { url = "https://files.pythonhosted.org/packages/84/17/93a00a8e03494a84db102dd0e3d35b8876b1084ca2c194b331a168d86eb1/libcst-1.9.0-cp315-cp315-win_arm64.whl", hash = "sha256:1260b5d070a3324447a53a00387225a55472a62077ce01922d30a2a78cc4b138", size = 2058633, upload-time = "2026-08-11T05:54:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/d7/00/c3751b12eb81822ea0b6131d374a98bc6c024c4b9ec5f6ea8f53dc23e967/libcst-1.9.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:02ee2dbdb5c218116f16a021350fc267a14be205b50d81c33f5d73857ab6fbf7", size = 2036178, upload-time = "2026-08-11T05:54:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/2f/12/48ff486eb5adc593f4818569e8896b74b672414ab4722a74b17e4c4cf31e/libcst-1.9.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:3e5b684191a0462b2d40a73261ea7f4da6a49e7a030b7e0fceca46bebb51dfe5", size = 2195496, upload-time = "2026-08-11T05:54:17.625Z" }, + { url = "https://files.pythonhosted.org/packages/51/18/b13a4669864d41a4801fed7b86ede1049dc9a1e1dde250a7ee1f9c3b1285/libcst-1.9.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:2b150c4f298fe54eb0fe73abf71f57db74d070796140a8c206c9615b6861f01e", size = 2245769, upload-time = "2026-08-11T05:54:19.343Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c7/cf2d46744825e84afbd7228fdeeea8d23cf6c4b2074253c27efb7705c653/libcst-1.9.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:50b913e37187f00a6fb88962009364e1cf1b1284aa1762304f34b52b972f2218", size = 2260576, upload-time = "2026-08-11T05:54:21.444Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/5433d3d62250b2e4325938db101766bcab2a006819684713bcccb6259a88/libcst-1.9.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:9f92c75283030fd58fb7d6e560168a00381e0e3368ec8a026a3ec8a828a05f93", size = 2368709, upload-time = "2026-08-11T05:54:23.276Z" }, + { url = "https://files.pythonhosted.org/packages/87/39/9f0e1690727623f99489895db0e42048a3e1e8cea0627517c593e437fd83/libcst-1.9.0-cp315-cp315t-win_amd64.whl", hash = "sha256:4adf97bb1aff8039b0bd4991e2f838be6e4fad552821b26b71a5d1a653c2582f", size = 2177866, upload-time = "2026-08-11T05:54:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/cd/79/9dc7811883e67ea771057e8937bc536eb3e77f3635fc5a93cd85b6fe1ea8/libcst-1.9.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8f0dd08a5773d7051e105250b2a2c73d8065ea9b2d68e7e1bdc5244660e75f93", size = 2052908, upload-time = "2026-08-11T05:54:26.835Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1086,6 +1147,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f94e2b1288c6886f13f34185e13117ed530f32b6f8a8/pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab", size = 141057, upload-time = "2025-06-10T15:32:15.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/ba/a067369fe61a2e57fb38732562927d5bae088c73cb9bb5438736a9555b29/pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6", size = 187027, upload-time = "2025-06-10T15:31:48.722Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" }, + { url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ac/c492a9da2e39abdff4c3094ec54acac9747743f36428281fb186a03fab76/pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96", size = 158779, upload-time = "2025-06-10T15:32:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9b/41998df3298960d7c67653669f37710fa2d568a5fc933ea24a6df60acaf6/pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb", size = 191331, upload-time = "2025-06-10T15:32:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" }, + { url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" }, + { url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" }, +] + [[package]] name = "pyzmq" version = "27.1.0"