From 8a3c29cb36ef79ccbdbd9349ffbcf75aa8745fda Mon Sep 17 00:00:00 2001 From: Ed Cuss <100875124+second-ed@users.noreply.github.com> Date: Fri, 15 May 2026 21:18:17 +0100 Subject: [PATCH 1/5] fix: correct typehint for sequence (#43) --- src/danom/_stream.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/danom/_stream.py b/src/danom/_stream.py index cdc07ae..2fce985 100644 --- a/src/danom/_stream.py +++ b/src/danom/_stream.py @@ -21,6 +21,7 @@ U = TypeVar("U") E = TypeVar("E") P = ParamSpec("P") +S = TypeVar("S", bound="_BaseStream") MapFn = Callable[P, U] FilterFn = Callable[P, bool] @@ -68,6 +69,11 @@ def fold( self, initial: T, fn: Callable[[T, U], T], *, workers: int = 1, use_threads: bool = False ) -> T: ... + @abstractmethod + def sequence( + self, *, workers: int = 1, use_threads: bool = False + ) -> Result[S, E] | Either[S, E]: ... + @abstractmethod def collect(self) -> tuple[U, ...]: ... @@ -337,8 +343,8 @@ def partition( return (Stream.from_iterable(pos), Stream.from_iterable(neg)) def sequence( - self, *, workers: int = 1, use_threads: bool = False - ) -> Result[T, E] | Either[T, E]: + self: Stream[T], *, workers: int = 1, use_threads: bool = False + ) -> Result[Stream[T], E] | Either[Stream[T], E]: """Convert a ``Stream`` of ``Result`` or ``Either`` monads to a monad of Stream .. doctest:: From 354c0d6954c9762e294cb57841fbd6991a598c94 Mon Sep 17 00:00:00 2001 From: ed cuss Date: Wed, 10 Jun 2026 21:34:14 +0100 Subject: [PATCH 2/5] fix: codemod for if else in loop --- src/danom/_stream.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/danom/_stream.py b/src/danom/_stream.py index 2fce985..9652a69 100644 --- a/src/danom/_stream.py +++ b/src/danom/_stream.py @@ -336,10 +336,10 @@ def partition( pos, neg = [], [] for x in seq_tuple: - if fn(x): - pos.append(x) - else: + if not fn(x): neg.append(x) + continue + pos.append(x) return (Stream.from_iterable(pos), Stream.from_iterable(neg)) def sequence( From 73ee72bc95c7732c5802cc0339822d2e0f6c05f4 Mon Sep 17 00:00:00 2001 From: Ed Cuss <100875124+second-ed@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:14:15 +0100 Subject: [PATCH 3/5] ci: add multiple python versions to ci (#44) * ci: add multiple python versions to ci * build: update repo-mapper-rs dev dep --- .github/workflows/ci_tests.yaml | 6 ++++++ pyproject.toml | 2 +- uv.lock | 18 +++++++++--------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml index 7ac68fa..2e0afd3 100644 --- a/.github/workflows/ci_tests.yaml +++ b/.github/workflows/ci_tests.yaml @@ -10,6 +10,11 @@ jobs: tests: name: python runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.12" + - "3.14" steps: - uses: actions/checkout@v6 @@ -17,6 +22,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v7 with: + python-version: ${{ matrix.python-version }} enable-cache: true cache-dependency-glob: "uv.lock" diff --git a/pyproject.toml b/pyproject.toml index d8c5fb9..27ce826 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dev = [ "pytest-asyncio>=1.3.0", "pytest-codspeed>=4.2.0", "pytest-cov>=7.0.0", - "repo-mapper-rs>=0.4.0", + "repo-mapper-rs>=0.4.1", "ruff>=0.14.6", "sphinx>=9.0.4", "ty>=0.0.8", diff --git a/uv.lock b/uv.lock index b71fdc0..2d3aabe 100644 --- a/uv.lock +++ b/uv.lock @@ -378,7 +378,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-codspeed", specifier = ">=4.2.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, - { name = "repo-mapper-rs", specifier = ">=0.4.0" }, + { name = "repo-mapper-rs", specifier = ">=0.4.1" }, { name = "ruff", specifier = ">=0.14.6" }, { name = "sphinx", specifier = ">=9.0.4" }, { name = "ty", specifier = ">=0.0.8" }, @@ -1127,16 +1127,16 @@ wheels = [ [[package]] name = "repo-mapper-rs" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/92/a86236660a5820e36b6bb9f76b3d15fb2169b7795a94ca84df0bb8ff409a/repo_mapper_rs-0.4.0.tar.gz", hash = "sha256:1b10da208bf18f6a9fcae168af8647afc35378756a64fb8b2d3dc7a56f12840b", size = 23838, upload-time = "2026-01-24T21:46:18.205Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/dc/3e87cd74c9aff611e5f6cc5f1ab03e66656e9669523a03053b6fc2cc6328/repo_mapper_rs-0.4.1.tar.gz", hash = "sha256:5295adaeba3d2acd91a1482779331add8c523e651224ccd1b243152015414228", size = 24844, upload-time = "2026-04-12T16:07:36.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/79/765fdd8680b2b314d4ce917aec5e0d73a3d95f5cd6137996310263dd1b41/repo_mapper_rs-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7016d60abe7077f4f2f8243909ba444c99b4e1d2d72a34fcb0ec53725c1ef2d5", size = 817597, upload-time = "2026-01-24T21:46:09.82Z" }, - { url = "https://files.pythonhosted.org/packages/7c/2a/eed5594e26ebc1ec186b813500ee9ec944154552c626ad5f1f1852e6c0c5/repo_mapper_rs-0.4.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:916aa59fea3307901c68e6d181c3fcd703f2f3170976ea4e4977aff5fbcc7a4e", size = 966002, upload-time = "2026-01-24T21:46:11.05Z" }, - { url = "https://files.pythonhosted.org/packages/77/3a/d8bf8055d1c5d1b25fe29745d050f1c14373d58a6a2ed3d375e2959f39f8/repo_mapper_rs-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:8d2fe43e5ccf6e33337ad81aa37874440574e5dc3ce673388c7e051a598c5638", size = 850188, upload-time = "2026-01-24T21:46:12.62Z" }, - { url = "https://files.pythonhosted.org/packages/fe/00/2e8f622d325fbd1887fad2b71323ce29e933851a60ce38d50dd27b136ae8/repo_mapper_rs-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bffde57100266dfbf0f1ec00354aff174bdc946093c11bd894930e33f8189600", size = 817748, upload-time = "2026-01-24T21:46:14.111Z" }, - { url = "https://files.pythonhosted.org/packages/33/0b/04195e7c3d962952b113cf0a15db041b2805997cfbe127919dc8dea1ffe4/repo_mapper_rs-0.4.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:78139929709acb97adf5cbe2126685f421cbc94565c56fcdc6a4a7789996e27c", size = 965450, upload-time = "2026-01-24T21:46:15.336Z" }, - { url = "https://files.pythonhosted.org/packages/80/b8/94e8fa92dcb29a1bcd5544b494714d4b76318b08b9778eab8e74e7bb37d5/repo_mapper_rs-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:ffc14e422fe1bfbc354a5987bf41b619594dec95387621a071b3e2e29fd4c764", size = 849603, upload-time = "2026-01-24T21:46:16.934Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/fc07c135242cd3c1eae5cda1fa48971fc4391110dc78186e7f8c232b14a1/repo_mapper_rs-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4f7953301db1926a48c90cef4136a1acb8c5a9cb28cf8ad049a74d10cc85c3fa", size = 593453, upload-time = "2026-04-12T16:07:27.403Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/a3517af5f1ad548c34791312bb6258132f603e7517c973087f9d2c02724a/repo_mapper_rs-0.4.1-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ce0b20149126f04367689b22b831dc587043832910d124bb7a75145c8b0c224c", size = 697599, upload-time = "2026-04-12T16:07:28.823Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/27a70a6bfa527e355a784b3d6be51af4786db13149b3954d1eb2c1bcea9f/repo_mapper_rs-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:c1c696b28e4bcc515b9a6b2426b1f218d1325a4c41db6469690d367a577fd60c", size = 582993, upload-time = "2026-04-12T16:07:30.04Z" }, + { url = "https://files.pythonhosted.org/packages/d6/03/f6ded0f6daad0f567ef2dec2376de79516cedbd3daf84a07652b219a2372/repo_mapper_rs-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:062158cf727eee7a1290da506139a5d468e0552148871481e00d976b12a7157b", size = 593375, upload-time = "2026-04-12T16:07:31.689Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/7352a105b8252b502a32bdf06139231347d500e32470aa6293dabba65191/repo_mapper_rs-0.4.1-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:733e25f7971ed2a7f14d30381cca46bdf7c35fa9c236ba1ac83178410326912d", size = 697466, upload-time = "2026-04-12T16:07:33.08Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/65d5f268cbc7c37abd72590661682985930103cd8400aa66e0f83964ad71/repo_mapper_rs-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:2aa8d336dcd5c656890bb9d0fd8995b013ff14336d4c9f14e41e6d1cb1e5ca5f", size = 582809, upload-time = "2026-04-12T16:07:34.884Z" }, ] [[package]] From e61246ec8cb136f59022364ef86190d02b03bffb Mon Sep 17 00:00:00 2001 From: Ed Cuss <100875124+second-ed@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:15:26 +0100 Subject: [PATCH 4/5] feat: safe handles subset of errors (#45) --- src/danom/_safe.py | 52 ++++++++++++++++++++++++++++++++++++++-------- tests/test_safe.py | 33 ++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/src/danom/_safe.py b/src/danom/_safe.py index 92ae598..118cc21 100644 --- a/src/danom/_safe.py +++ b/src/danom/_safe.py @@ -1,7 +1,7 @@ import functools import traceback from collections.abc import Callable -from typing import Concatenate, ParamSpec, TypeVar +from typing import Concatenate, ParamSpec, TypeVar, overload from danom._result import Err, Ok, Result @@ -10,8 +10,22 @@ U = TypeVar("U") E = TypeVar("E") +type ExceptionType = type[Exception] | tuple[type[Exception], ...] -def safe[**P, U](func: Callable[P, U]) -> Callable[P, Result[U, Exception]]: + +@overload +def safe[**P, U](func: Callable[P, U]) -> Callable[P, Result[U, Exception]]: ... + + +@overload +def safe( + func: None = None, *, errors: ExceptionType = Exception +) -> Callable[[Callable[P, U]], Callable[P, Result[U, Exception]]]: ... + + +def safe[**P, U]( + func: Callable[P, U] | None = None, *, errors: ExceptionType = Exception +) -> Callable[P, Result[U, Exception]] | Callable[..., Callable[P, Result[U, Exception]]]: """Decorator for functions that wraps the function in a try except returns `Ok` on success else `Err`. .. code-block:: python @@ -23,16 +37,36 @@ def add_one(a: int) -> int: return a + 1 add_one(1) == Ok(inner=2) + + + Only catch a single error type or subset of error types by passing in an error type to catch. + + .. code-block:: python + + from danom import safe + + @safe(errors=ZeroDivisionError) + def div(a: int, b: int) -> float: + return a / b + + + div(2, 0) == Err(error=ZeroDivisionError('division by zero')) + div(2, "") + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) """ - @functools.wraps(func) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[U, Exception]: - try: - return Ok(func(*args, **kwargs)) - except Exception as e: # noqa: BLE001 - return Err(error=e, input_args=(args, kwargs), traceback=traceback.format_exc()) # ty: ignore[invalid-return-type] + 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)) + except errors as e: + return Err(error=e, input_args=(args, kwargs), traceback=traceback.format_exc()) # ty: ignore[invalid-return-type] - return wrapper + return wrapper + + return decorator(func) if func is not None else decorator def safe_method[T, **P, U]( diff --git a/tests/test_safe.py b/tests/test_safe.py index fe9bb84..504323b 100644 --- a/tests/test_safe.py +++ b/tests/test_safe.py @@ -1,8 +1,10 @@ +from contextlib import nullcontext from functools import partial import pytest from danom._result import Err, Ok +from danom._safe import safe from tests.conftest import ( REPO_ROOT, Adder, @@ -65,7 +67,7 @@ def test_traceback(): expected_lines = [ "Traceback (most recent call last):", - ' File "./src/danom/_safe.py", line 31, in wrapper', + ' File "./src/danom/_safe.py", line 63, in wrapper', " return Ok(func(*args, **kwargs))", ' File "./tests/conftest.py", line 117, in div_zero', " return x / 0", @@ -85,3 +87,32 @@ def test_traceback(): def test_safe_on_method(): cls = Adder() assert cls.safe_add(2, 2) == Ok(4) + + +@pytest.mark.parametrize( + ("a", "b", "errors", "expected_result", "expected_context"), + [ + pytest.param( + 1, 1, Exception, True, nullcontext(), id="Does nothing if the function returns ok" + ), + pytest.param( + 1, + 0, + ZeroDivisionError, + False, + nullcontext(), + id="Returns Err if the err type is in the passed in list of errors", + ), + pytest.param( + 1, + "", + ZeroDivisionError, + True, + pytest.raises(TypeError), + id="Raises exception if the exception type isn't in the list of errors", + ), + ], +) +def test_safe_subset_of_errors(a, b, errors, expected_result, expected_context) -> None: + with expected_context: + assert safe(errors=errors)(lambda a, b: a / b)(a, b).is_ok() is expected_result From 61c3ee2378cbf09d6a72efdf1a3a9aee9705d88e Mon Sep 17 00:00:00 2001 From: ed cuss Date: Mon, 29 Jun 2026 20:16:33 +0100 Subject: [PATCH 5/5] bump: => 0.15.0 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 27ce826..8e9c09f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "danom" -version = "0.14.1" +version = "0.15.0" description = "Functional streams and monads" readme = "README.md" license = "MIT" diff --git a/uv.lock b/uv.lock index 2d3aabe..4d08814 100644 --- a/uv.lock +++ b/uv.lock @@ -343,7 +343,7 @@ wheels = [ [[package]] name = "danom" -version = "0.14.1" +version = "0.15.0" source = { editable = "." } dependencies = [ { name = "attrs" },