Skip to content

Commit 70168ca

Browse files
committed
feat: added a few decorators
1 parent 8533b36 commit 70168ca

File tree

10 files changed

+870
-38
lines changed

10 files changed

+870
-38
lines changed

tasks/tasks-0001-prd-klaw-result.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,19 +122,19 @@
122122
- [x] 2.19 Add `__repr__`, `__eq__`, `__hash__` via msgspec.Struct defaults
123123
- [x] 2.20 Export all types from `types/__init__.py`
124124

125-
- [ ] 3.0 Decorators & Propagation
126-
127-
- [ ] 3.1 Implement `@safe` decorator using `wrapt.decorator` — catches exceptions, returns `Err(exception)`
128-
- [ ] 3.2 Implement `@safe_async` decorator for async functions
129-
- [ ] 3.3 Implement `@pipe` decorator — wraps return value in `Ok()`
130-
- [ ] 3.4 Implement `@pipe_async` decorator for async functions
131-
- [ ] 3.5 Implement `@result` decorator — catches `Propagate` exception, returns contained `Err`
132-
- [ ] 3.6 Implement `@result` for async functions (same decorator, detect async)
133-
- [ ] 3.7 Implement `@do` decorator — generator-based do-notation with `yield` for Result extraction
134-
- [ ] 3.8 Implement `@do_async` decorator — async generator do-notation
135-
- [ ] 3.9 Ensure all decorators preserve function signatures and type hints via `wrapt`
136-
- [ ] 3.10 Add context manager support to Result/Option for `with result.ctx as value:` unwrapping
137-
- [ ] 3.11 Export all decorators from `decorators/__init__.py`
125+
- [x] 3.0 Decorators & Propagation
126+
127+
- [x] 3.1 Implement `@safe` decorator using `wrapt.decorator` — catches exceptions, returns `Err(exception)`
128+
- [x] 3.2 Implement `@safe_async` decorator for async functions
129+
- [x] 3.3 Implement `@pipe` decorator — wraps return value in `Ok()`
130+
- [x] 3.4 Implement `@pipe_async` decorator for async functions
131+
- [x] 3.5 Implement `@result` decorator — catches `Propagate` exception, returns contained `Err`
132+
- [x] 3.6 Implement `@result` for async functions (same decorator, detect async)
133+
- [x] 3.7 Implement `@do` decorator — generator-based do-notation with `yield` for Result extraction
134+
- [x] 3.8 Implement `@do_async` decorator — async generator do-notation
135+
- [x] 3.9 Ensure all decorators preserve function signatures and type hints via `wrapt`
136+
- [x] 3.10 Add context manager support to Result/Option for `with result.ctx as value:` unwrapping
137+
- [x] 3.11 Export all decorators from `decorators/__init__.py`
138138

139139
- [ ] 4.0 Composition & Operators
140140

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,135 @@
11
"""@do and @do_async decorators for generator-based do-notation."""
22

3-
# Placeholder - implementation in Task 3.0
3+
from __future__ import annotations
4+
5+
from collections.abc import AsyncGenerator, Callable, Generator
6+
from typing import Any, ParamSpec, TypeVar
7+
8+
import wrapt
9+
10+
from klaw_result.types.option import NothingType, Some
11+
from klaw_result.types.result import Err, Ok
412

513
__all__ = ["do", "do_async"]
614

15+
P = ParamSpec("P")
16+
T = TypeVar("T")
17+
E = TypeVar("E")
18+
19+
20+
def do(
21+
func: Callable[P, Generator[Ok[Any] | Err[E] | Some[Any] | NothingType, Any, T]],
22+
) -> Callable[P, Ok[T] | Err[E]]:
23+
"""Decorator for generator-based do-notation with Result.
24+
25+
Enables Haskell-style do-notation using generators. Yield Result values
26+
to extract their Ok values; if an Err is yielded, it short-circuits and
27+
returns that Err immediately.
28+
29+
The generator should yield Result values and return the final value
30+
to be wrapped in Ok.
31+
32+
Args:
33+
func: A generator function that yields Results and returns T.
34+
35+
Returns:
36+
A function that returns Result[T, E].
37+
38+
Examples:
39+
>>> @do
40+
... def compute() -> Generator[Result[int, str], int, int]:
41+
... x = yield get_x() # Returns Err early if get_x() is Err
42+
... y = yield get_y() # Returns Err early if get_y() is Err
43+
... return x + y
44+
>>> compute()
45+
Ok(value=...) # or Err(...) if any step failed
46+
"""
47+
48+
@wrapt.decorator
49+
def wrapper(
50+
wrapped: Callable[
51+
P, Generator[Ok[Any] | Err[E] | Some[Any] | NothingType, Any, T]
52+
],
53+
instance: Any, # noqa: ARG001
54+
args: tuple[Any, ...],
55+
kwargs: dict[str, Any],
56+
) -> Ok[T] | Err[E]:
57+
gen = wrapped(*args, **kwargs)
58+
try:
59+
result = next(gen)
60+
while True:
61+
if isinstance(result, Err):
62+
return result
63+
if isinstance(result, NothingType):
64+
return Err(None) # type: ignore[arg-type]
65+
if isinstance(result, Ok):
66+
value = result.value
67+
elif isinstance(result, Some):
68+
value = result.value
69+
else:
70+
value = result
71+
result = gen.send(value)
72+
except StopIteration as e:
73+
return Ok(e.value)
74+
75+
return wrapper(func) # type: ignore[return-value]
76+
77+
78+
def do_async(
79+
func: Callable[P, AsyncGenerator[Ok[Any] | Err[E] | Some[Any] | NothingType, Any]],
80+
) -> Callable[P, Any]:
81+
"""Async decorator for generator-based do-notation with Result.
82+
83+
Enables Haskell-style do-notation using async generators. Yield Result
84+
values to extract their Ok values; if an Err is yielded, it short-circuits
85+
and returns that Err immediately.
86+
87+
Note: Async generators cannot have a return value in Python, so the last
88+
yielded Ok value is used as the final result.
89+
90+
Args:
91+
func: An async generator function that yields Results.
92+
93+
Returns:
94+
An async function that returns Result[T, E].
795
8-
def do(fn):
9-
"""Placeholder for @do decorator."""
10-
return fn
96+
Examples:
97+
>>> @do_async
98+
... async def compute():
99+
... x = yield await fetch_x() # Returns Err early if fetch_x() is Err
100+
... y = yield await fetch_y() # Returns Err early if fetch_y() is Err
101+
... yield Ok(x + y) # Final result
102+
"""
11103

104+
@wrapt.decorator
105+
async def wrapper(
106+
wrapped: Callable[
107+
P, AsyncGenerator[Ok[Any] | Err[E] | Some[Any] | NothingType, Any]
108+
],
109+
instance: Any, # noqa: ARG001
110+
args: tuple[Any, ...],
111+
kwargs: dict[str, Any],
112+
) -> Ok[T] | Err[E]:
113+
gen = wrapped(*args, **kwargs)
114+
last_ok: Ok[Any] | None = None
115+
try:
116+
result = await gen.asend(None)
117+
while True:
118+
if isinstance(result, Err):
119+
return result
120+
if isinstance(result, NothingType):
121+
return Err(None) # type: ignore[arg-type]
122+
if isinstance(result, Ok):
123+
last_ok = result
124+
value = result.value
125+
elif isinstance(result, Some):
126+
value = result.value
127+
else:
128+
value = result
129+
result = await gen.asend(value)
130+
except StopAsyncIteration:
131+
if last_ok is not None:
132+
return last_ok
133+
return Ok(None) # type: ignore[arg-type]
12134

13-
def do_async(fn):
14-
"""Placeholder for @do_async decorator."""
15-
return fn
135+
return wrapper(func)
Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,82 @@
11
"""@pipe and @pipe_async decorators for wrapping return values in Ok."""
22

3-
# Placeholder - implementation in Task 3.0
3+
from __future__ import annotations
4+
5+
from collections.abc import Awaitable, Callable
6+
from typing import Any, ParamSpec, TypeVar
7+
8+
import wrapt
9+
10+
from klaw_result.types.result import Ok
411

512
__all__ = ["pipe", "pipe_async"]
613

14+
P = ParamSpec("P")
15+
T = TypeVar("T")
16+
17+
18+
def pipe(func: Callable[P, T]) -> Callable[P, Ok[T]]:
19+
"""Decorator that wraps the return value in Ok.
20+
21+
Useful for functions that never fail, to make them compatible
22+
with Result-based pipelines.
23+
24+
Args:
25+
func: The function to wrap.
26+
27+
Returns:
28+
A wrapped function that returns Ok[T] instead of T.
29+
30+
Examples:
31+
>>> @pipe
32+
... def add(a: int, b: int) -> int:
33+
... return a + b
34+
>>> add(2, 3)
35+
Ok(value=5)
36+
"""
37+
38+
@wrapt.decorator
39+
def wrapper(
40+
wrapped: Callable[P, T],
41+
instance: Any, # noqa: ARG001
42+
args: tuple[Any, ...],
43+
kwargs: dict[str, Any],
44+
) -> Ok[T]:
45+
return Ok(wrapped(*args, **kwargs))
46+
47+
return wrapper(func) # type: ignore[return-value]
48+
49+
50+
def pipe_async(
51+
func: Callable[P, Awaitable[T]],
52+
) -> Callable[P, Awaitable[Ok[T]]]:
53+
"""Async decorator that wraps the return value in Ok.
54+
55+
Useful for async functions that never fail, to make them compatible
56+
with Result-based pipelines.
57+
58+
Args:
59+
func: The async function to wrap.
60+
61+
Returns:
62+
A wrapped async function that returns Ok[T] instead of T.
763
8-
def pipe(fn):
9-
"""Placeholder for @pipe decorator."""
10-
return fn
64+
Examples:
65+
>>> @pipe_async
66+
... async def fetch_data() -> str:
67+
... return "data"
68+
>>> await fetch_data()
69+
Ok(value='data')
70+
"""
1171

72+
@wrapt.decorator
73+
async def wrapper(
74+
wrapped: Callable[P, Awaitable[T]],
75+
instance: Any, # noqa: ARG001
76+
args: tuple[Any, ...],
77+
kwargs: dict[str, Any],
78+
) -> Ok[T]:
79+
result = await wrapped(*args, **kwargs)
80+
return Ok(result)
1281

13-
def pipe_async(fn):
14-
"""Placeholder for @pipe_async decorator."""
15-
return fn
82+
return wrapper(func) # type: ignore[return-value]
Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,77 @@
11
"""@result decorator for catching Propagate exceptions."""
22

3-
# Placeholder - implementation in Task 3.0
3+
from __future__ import annotations
4+
5+
import asyncio
6+
from collections.abc import Awaitable, Callable
7+
from typing import Any, ParamSpec, TypeVar
8+
9+
import wrapt
10+
11+
from klaw_result.types.propagate import Propagate
12+
from klaw_result.types.result import Err, Ok
413

514
__all__ = ["result"]
615

16+
P = ParamSpec("P")
17+
T = TypeVar("T")
18+
E = TypeVar("E")
19+
20+
21+
def result(
22+
func: Callable[P, Ok[T] | Err[E]] | Callable[P, Awaitable[Ok[T] | Err[E]]],
23+
) -> Callable[P, Ok[T] | Err[E]] | Callable[P, Awaitable[Ok[T] | Err[E]]]:
24+
"""Decorator that catches Propagate exceptions for .bail() support.
25+
26+
When a function decorated with @result calls .bail() on an Err,
27+
the Propagate exception is caught and the Err is returned.
28+
This enables Rust-like ? operator semantics.
29+
30+
Automatically detects async functions and handles them appropriately.
31+
32+
Args:
33+
func: The function to wrap. Must return a Result type.
34+
35+
Returns:
36+
A wrapped function that catches Propagate and returns the contained Err.
37+
38+
Examples:
39+
>>> @result
40+
... def process(x: int) -> Result[int, str]:
41+
... value = get_value(x).bail() # Returns Err early if get_value fails
42+
... return Ok(value * 2)
43+
44+
>>> @result
45+
... async def async_process(x: int) -> Result[int, str]:
46+
... value = (await fetch(x)).bail()
47+
... return Ok(value * 2)
48+
"""
49+
if asyncio.iscoroutinefunction(func):
50+
51+
@wrapt.decorator
52+
async def async_wrapper(
53+
wrapped: Callable[P, Awaitable[Ok[T] | Err[E]]],
54+
instance: Any, # noqa: ARG001
55+
args: tuple[Any, ...],
56+
kwargs: dict[str, Any],
57+
) -> Ok[T] | Err[E]:
58+
try:
59+
return await wrapped(*args, **kwargs)
60+
except Propagate as p:
61+
return p.value # type: ignore[return-value]
62+
63+
return async_wrapper(func) # type: ignore[return-value]
64+
65+
@wrapt.decorator
66+
def sync_wrapper(
67+
wrapped: Callable[P, Ok[T] | Err[E]],
68+
instance: Any, # noqa: ARG001
69+
args: tuple[Any, ...],
70+
kwargs: dict[str, Any],
71+
) -> Ok[T] | Err[E]:
72+
try:
73+
return wrapped(*args, **kwargs)
74+
except Propagate as p:
75+
return p.value # type: ignore[return-value]
776

8-
def result(fn):
9-
"""Placeholder for @result decorator."""
10-
return fn
77+
return sync_wrapper(func) # type: ignore[return-value]

0 commit comments

Comments
 (0)