Skip to content

Commit 3cf6a2c

Browse files
committed
Add ~ operator for simple inline null coalescing
1 parent 41f513d commit 3cf6a2c

File tree

3 files changed

+22
-13
lines changed

3 files changed

+22
-13
lines changed

better_functools/apply.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from dataclasses import dataclass
44
from typing import (
5-
TYPE_CHECKING,
65
Any,
76
Callable,
87
Concatenate,
@@ -36,27 +35,35 @@
3635
Ts = TypeVarTuple("Ts")
3736

3837

39-
class apply(Generic[T_APPLY]):
38+
class apply(Generic[T, R]):
4039
"""Make a function callable by using `@` operator.
4140
4241
This is the `@` version of `... | fn` in `better_functools.pipe.Composition`.
4342
4443
>>> "1234" @ apply(int)
4544
1234
45+
46+
prepending with `~` will return the null coalescing version.
47+
48+
>>> None @ ~apply(int) is None
49+
True
4650
"""
4751

48-
def __init__(self, fn: T_APPLY) -> None:
52+
def __init__(self, fn: Callable[[T], R]) -> None:
4953
self.fn = fn
5054

51-
if TYPE_CHECKING:
52-
__call__: T_APPLY
53-
__rmatmul__: T_APPLY
54-
else:
55+
def __call__(self, val: T) -> R:
56+
return self.fn(val)
5557

56-
def __call__(self, *args, **kwargs):
57-
return self.fn(*args, **kwargs)
58+
__rmatmul__ = __call__
5859

59-
__rmatmul__ = __call__
60+
def __invert__(self) -> apply[T | None, R | None]:
61+
def _fn(v: T | None) -> R | None:
62+
if v is None:
63+
return None
64+
return self.fn(v)
65+
66+
return apply(_fn)
6067

6168

6269
@dataclass
@@ -235,7 +242,7 @@ def __rmatmul__(self, left: T | None) -> T | Self:
235242
"""
236243

237244

238-
def static(fn: Callable[Concatenate[T, P], R]) -> Callable[P, apply[Callable[[T], R]]]:
245+
def static(fn: Callable[Concatenate[T, P], R]) -> Callable[P, apply[T, R]]:
239246
"""*Experimental*: Make a bound method static.
240247
241248
This makes it easier to chain the method.
@@ -259,7 +266,7 @@ def static(fn: Callable[Concatenate[T, P], R]) -> Callable[P, apply[Callable[[T]
259266
- Does not work well with MyPy.
260267
"""
261268

262-
def _outer(*args: P.args, **kwargs: P.kwargs) -> apply[Callable[[T], R]]:
269+
def _outer(*args: P.args, **kwargs: P.kwargs) -> apply[T, R]:
263270
@apply
264271
def _inner(first: T) -> R:
265272
return fn(first, *args, **kwargs)

tests/test_apply.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ def fetch(v: int, return_none: bool) -> int | None:
3333
(lambda: assert_type(mod @ star_args @ invoke((3, 2)), int), 1),
3434
(lambda: assert_type(add @ compose(mul @ bind(2)) @ invoke(1, 2), int), 6),
3535
(lambda: assert_type({"id": 1234} @ static(dict[str, int].get)("id"), int | None), 1234),
36+
(lambda: assert_type({"a": 1}.get("b") @ ~apply(add @ bind(1)), int | None), None),
37+
(lambda: assert_type({"a": 1}.get("a") @ ~apply(add @ bind(1)), int | None), 2),
3638
],
3739
)
3840
def test_expressions(expression: Callable[[], Any], expected: Any) -> None:

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)