trcks 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trcks/__init__.py +123 -0
- trcks/_typing.py +31 -0
- trcks/fp/__init__.py +64 -0
- trcks/fp/composition.py +268 -0
- trcks/fp/monads/__init__.py +3 -0
- trcks/fp/monads/awaitable.py +182 -0
- trcks/fp/monads/awaitable_result.py +561 -0
- trcks/fp/monads/result.py +243 -0
- trcks/oop.py +1590 -0
- trcks/py.typed +0 -0
- trcks-0.2.0.dist-info/METADATA +1004 -0
- trcks-0.2.0.dist-info/RECORD +14 -0
- trcks-0.2.0.dist-info/WHEEL +4 -0
- trcks-0.2.0.dist-info/licenses/LICENSE +21 -0
trcks/__init__.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Typesafe railway-oriented programming (ROP).
|
|
2
|
+
|
|
3
|
+
This package provides
|
|
4
|
+
|
|
5
|
+
- the generic (return) types `trcks.Result` and `trcks.AwaitableResult` and
|
|
6
|
+
- the subpackages `trcks.fp` and `trcks.oop` for working with these types
|
|
7
|
+
in a functional and object-oriented way, respectively.
|
|
8
|
+
|
|
9
|
+
See:
|
|
10
|
+
https://fsharpforfunandprofit.com/posts/recipe-part2/
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from collections.abc import Awaitable
|
|
14
|
+
from typing import Literal, Union
|
|
15
|
+
|
|
16
|
+
from trcks._typing import TypeAlias, TypeVar
|
|
17
|
+
|
|
18
|
+
__docformat__ = "google"
|
|
19
|
+
|
|
20
|
+
_F_co = TypeVar("_F_co", covariant=True)
|
|
21
|
+
_S_co = TypeVar("_S_co", covariant=True)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
Failure: TypeAlias = tuple[Literal["failure"], _F_co]
|
|
25
|
+
"""`tuple` of length 2 containing ``"failure"`` followed by a value of type `_F_co`.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> failure: Failure[str] = ("failure", "File does not exist")
|
|
29
|
+
|
|
30
|
+
Note:
|
|
31
|
+
This generic type is called "Left" in
|
|
32
|
+
some functional programming languages and packages (e.g. Haskell and fp-ts).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
Success: TypeAlias = tuple[Literal["success"], _S_co]
|
|
36
|
+
"""`tuple` of length 2 containing ``"success"`` followed by a value of type `_S_co`.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
>>> success: Success[int] = ("success", 42)
|
|
40
|
+
|
|
41
|
+
Note:
|
|
42
|
+
This generic type is called "Right" in
|
|
43
|
+
some functional programming languages and packages (e.g. Haskell and fp-ts).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
Result: TypeAlias = Union[Failure[_F_co], Success[_S_co]]
|
|
47
|
+
"""Discriminated union of the generic types `_F_co` and `_S_co`.
|
|
48
|
+
|
|
49
|
+
Can be used as a return type of a function
|
|
50
|
+
instead of returning `_S_co` and raising `_F_co`.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
>>> def divide(a: float, b: float) -> Result[ZeroDivisionError, float]:
|
|
54
|
+
... try:
|
|
55
|
+
... return ("success", a/b)
|
|
56
|
+
... except ZeroDivisionError as e:
|
|
57
|
+
... return ("failure", e)
|
|
58
|
+
...
|
|
59
|
+
>>> divide(5.0, 2.0)
|
|
60
|
+
('success', 2.5)
|
|
61
|
+
>>> divide(3.5, 0.0)
|
|
62
|
+
('failure', ZeroDivisionError('float division by zero'))
|
|
63
|
+
|
|
64
|
+
Note:
|
|
65
|
+
This generic type is called "Either" in
|
|
66
|
+
some functional programming languages and packages (e.g. Haskell and fp-ts).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
AwaitableFailure: TypeAlias = Awaitable[Failure[_F_co]]
|
|
70
|
+
"""`collections.abc.Awaitable` that returns a `Failure`
|
|
71
|
+
when used in an ``await`` expression.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
AwaitableSuccess: TypeAlias = Awaitable[Success[_S_co]]
|
|
75
|
+
"""`collections.abc.Awaitable` that returns a `Success`
|
|
76
|
+
when used in an ``await`` expression.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
AwaitableResult: TypeAlias = Awaitable[Result[_F_co, _S_co]]
|
|
80
|
+
"""`collections.abc.Awaitable` that returns a `Result`
|
|
81
|
+
when used in an ``await`` expression.
|
|
82
|
+
|
|
83
|
+
Examples:
|
|
84
|
+
Can be used to annotate the non-awaited return value of an async function:
|
|
85
|
+
|
|
86
|
+
>>> import asyncio
|
|
87
|
+
>>> async def divide_slowly(
|
|
88
|
+
... a: float, b: float
|
|
89
|
+
... ) -> Result[ZeroDivisionError, float]:
|
|
90
|
+
... await asyncio.sleep(0.001)
|
|
91
|
+
... try:
|
|
92
|
+
... return ("success", a / b)
|
|
93
|
+
... except ZeroDivisionError as e:
|
|
94
|
+
... return ("failure", e)
|
|
95
|
+
...
|
|
96
|
+
>>> async def main() -> None:
|
|
97
|
+
... a_rslt: AwaitableResult[ZeroDivisionError, float] = (
|
|
98
|
+
... divide_slowly(3.0, 0.0)
|
|
99
|
+
... )
|
|
100
|
+
... rslt: Result[ZeroDivisionError, float] = await a_rslt
|
|
101
|
+
... print(rslt)
|
|
102
|
+
...
|
|
103
|
+
>>> asyncio.run(main())
|
|
104
|
+
('failure', ZeroDivisionError('float division by zero'))
|
|
105
|
+
|
|
106
|
+
Can also be used to annotate an async function:
|
|
107
|
+
|
|
108
|
+
>>> import asyncio
|
|
109
|
+
>>> from collections.abc import Callable
|
|
110
|
+
>>>
|
|
111
|
+
>>> async def divide_slowly(
|
|
112
|
+
... a: float, b: float
|
|
113
|
+
... ) -> Result[ZeroDivisionError, float]:
|
|
114
|
+
... await asyncio.sleep(0.001)
|
|
115
|
+
... try:
|
|
116
|
+
... return ("success", a / b)
|
|
117
|
+
... except ZeroDivisionError as e:
|
|
118
|
+
... return ("failure", e)
|
|
119
|
+
...
|
|
120
|
+
>>> copy_of_divide_slowly: Callable[
|
|
121
|
+
... [float, float], AwaitableResult[ZeroDivisionError, float]
|
|
122
|
+
... ] = divide_slowly
|
|
123
|
+
"""
|
trcks/_typing.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Recent features from `typing`.
|
|
2
|
+
|
|
3
|
+
Imported from `typing_extensions` if necessary in older Python versions.
|
|
4
|
+
This helps to avoid `sys.version_info` checks in the codebase.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
if sys.version_info >= (3, 13): # pragma: no cover
|
|
10
|
+
from typing import TypeVar # Argument "default" has been added in Python 3.13.
|
|
11
|
+
else: # pragma: no cover
|
|
12
|
+
from typing_extensions import TypeVar
|
|
13
|
+
|
|
14
|
+
if sys.version_info >= (3, 12): # pragma: no cover
|
|
15
|
+
from typing import override
|
|
16
|
+
else: # pragma: no cover
|
|
17
|
+
from typing_extensions import override
|
|
18
|
+
|
|
19
|
+
if sys.version_info >= (3, 11): # pragma: no cover
|
|
20
|
+
from typing import Never, TypeAlias, assert_never
|
|
21
|
+
else: # pragma: no cover
|
|
22
|
+
from typing_extensions import Never, TypeAlias, assert_never
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Never",
|
|
26
|
+
"TypeAlias",
|
|
27
|
+
"TypeVar",
|
|
28
|
+
"assert_never",
|
|
29
|
+
"override",
|
|
30
|
+
]
|
|
31
|
+
__docformat__ = "google"
|
trcks/fp/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Functional interface for `trcks`.
|
|
2
|
+
|
|
3
|
+
This package provides functions for processing values of the following types
|
|
4
|
+
in a functional style:
|
|
5
|
+
|
|
6
|
+
- `collections.abc.Awaitable`
|
|
7
|
+
- `trcks.AwaitableResult`
|
|
8
|
+
- `trcks.Result`
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
This example uses the modules `trcks.fp.composition` and `trcks.fp.monads.result`
|
|
12
|
+
to create and further process a value of type `trcks.Result`:
|
|
13
|
+
|
|
14
|
+
>>> import math
|
|
15
|
+
>>> from typing import Literal
|
|
16
|
+
>>> from trcks import Result
|
|
17
|
+
>>> from trcks.fp.composition import pipe
|
|
18
|
+
>>> from trcks.fp.monads import result as r
|
|
19
|
+
>>> GetSquareRootResult = Result[Literal["negative value"], float]
|
|
20
|
+
>>> def get_square_root(x: float) -> GetSquareRootResult:
|
|
21
|
+
... return pipe(
|
|
22
|
+
... (
|
|
23
|
+
... x,
|
|
24
|
+
... lambda xx:
|
|
25
|
+
... ("success", xx)
|
|
26
|
+
... if xx >= 0
|
|
27
|
+
... else ("failure", "negative value"),
|
|
28
|
+
... r.map_success(math.sqrt),
|
|
29
|
+
... )
|
|
30
|
+
... )
|
|
31
|
+
...
|
|
32
|
+
>>> get_square_root(25.0)
|
|
33
|
+
('success', 5.0)
|
|
34
|
+
>>> get_square_root(-25.0)
|
|
35
|
+
('failure', 'negative value')
|
|
36
|
+
|
|
37
|
+
If your static type checker cannot infer the type of
|
|
38
|
+
the argument passed to `trcks.fp.composition.pipe`,
|
|
39
|
+
you can explicitly assign a type:
|
|
40
|
+
|
|
41
|
+
>>> import math
|
|
42
|
+
>>> from typing import Literal
|
|
43
|
+
>>> from trcks import Result
|
|
44
|
+
>>> from trcks.fp.composition import Pipeline2, pipe
|
|
45
|
+
>>> from trcks.fp.monads import result as r
|
|
46
|
+
>>> GetSquareRootResult = Result[Literal["negative value"], float]
|
|
47
|
+
>>> def get_square_root(x: float) -> GetSquareRootResult:
|
|
48
|
+
... p: Pipeline2[float, GetSquareRootResult, GetSquareRootResult] = (
|
|
49
|
+
... x,
|
|
50
|
+
... lambda xx:
|
|
51
|
+
... ("success", xx)
|
|
52
|
+
... if xx >= 0
|
|
53
|
+
... else ("failure", "negative value"),
|
|
54
|
+
... r.map_success(math.sqrt),
|
|
55
|
+
... )
|
|
56
|
+
... return pipe(p)
|
|
57
|
+
...
|
|
58
|
+
>>> get_square_root(25.0)
|
|
59
|
+
('success', 5.0)
|
|
60
|
+
>>> get_square_root(-25.0)
|
|
61
|
+
('failure', 'negative value')
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
__docformat__ = "google"
|
trcks/fp/composition.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Types and higher order functions for function composition.
|
|
2
|
+
|
|
3
|
+
Attributes:
|
|
4
|
+
Composable:
|
|
5
|
+
Up to seven compatible functions that can be applied sequentially
|
|
6
|
+
from first to last.
|
|
7
|
+
Composable1:
|
|
8
|
+
A single function.
|
|
9
|
+
Composable2:
|
|
10
|
+
Two compatible functions that can be applied sequentially from first to last.
|
|
11
|
+
Composable3:
|
|
12
|
+
Three compatible functions that can be applied sequentially from first to last.
|
|
13
|
+
Composable4:
|
|
14
|
+
Four compatible functions that can be applied sequentially from first to last.
|
|
15
|
+
Composable5:
|
|
16
|
+
Five compatible functions that can be applied sequentially from first to last.
|
|
17
|
+
Composable6:
|
|
18
|
+
Six compatible functions that can be applied sequentially from first to last.
|
|
19
|
+
Composable7:
|
|
20
|
+
Seven compatible functions that can be applied sequentially from first to last.
|
|
21
|
+
Pipeline:
|
|
22
|
+
A single value followed by up to seven compatible functions
|
|
23
|
+
that can be applied sequentially from first to last.
|
|
24
|
+
Pipeline0:
|
|
25
|
+
A single value.
|
|
26
|
+
Pipeline1:
|
|
27
|
+
A single value followed by a single compatible function
|
|
28
|
+
that can be applied.
|
|
29
|
+
Pipeline2:
|
|
30
|
+
A single value followed by two compatible functions
|
|
31
|
+
that can be applied sequentially from first to last.
|
|
32
|
+
Pipeline3:
|
|
33
|
+
A single value followed by three compatible functions
|
|
34
|
+
that can be applied sequentially from first to last.
|
|
35
|
+
Pipeline4:
|
|
36
|
+
A single value followed by four compatible functions
|
|
37
|
+
that can be applied sequentially from first to last.
|
|
38
|
+
Pipeline5:
|
|
39
|
+
A single value followed by five compatible functions
|
|
40
|
+
that can be applied sequentially from first to last.
|
|
41
|
+
Pipeline6:
|
|
42
|
+
A single value followed by six compatible functions
|
|
43
|
+
that can be applied sequentially from first to last.
|
|
44
|
+
Pipeline7:
|
|
45
|
+
A single value followed by seven compatible functions
|
|
46
|
+
that can be applied sequentially from first to last.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
Sequentially apply two functions to one input value
|
|
50
|
+
in three different ways:
|
|
51
|
+
|
|
52
|
+
>>> def to_length_string(n: int) -> str:
|
|
53
|
+
... return f"Length: {n}"
|
|
54
|
+
...
|
|
55
|
+
>>> input_ = "Hello, world!"
|
|
56
|
+
>>> to_length_string(len(input_))
|
|
57
|
+
'Length: 13'
|
|
58
|
+
>>> get_length_string = compose((len, to_length_string))
|
|
59
|
+
>>> get_length_string(input_)
|
|
60
|
+
'Length: 13'
|
|
61
|
+
>>> pipe((input_, len, to_length_string))
|
|
62
|
+
'Length: 13'
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
from __future__ import annotations
|
|
66
|
+
|
|
67
|
+
from collections.abc import Callable
|
|
68
|
+
from typing import Union
|
|
69
|
+
|
|
70
|
+
from trcks._typing import TypeAlias, TypeVar, assert_never
|
|
71
|
+
|
|
72
|
+
__docformat__ = "google"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_IN = TypeVar("_IN")
|
|
76
|
+
_OUT = TypeVar("_OUT")
|
|
77
|
+
_T0 = TypeVar("_T0")
|
|
78
|
+
_T1 = TypeVar("_T1")
|
|
79
|
+
_T2 = TypeVar("_T2")
|
|
80
|
+
_T3 = TypeVar("_T3")
|
|
81
|
+
_T4 = TypeVar("_T4")
|
|
82
|
+
_T5 = TypeVar("_T5")
|
|
83
|
+
_T6 = TypeVar("_T6")
|
|
84
|
+
_T7 = TypeVar("_T7")
|
|
85
|
+
|
|
86
|
+
# Tuple type unpacking does not work correctly in Python 3.9 and 3.10
|
|
87
|
+
# (see https://github.com/python/typing_extensions/issues/103).
|
|
88
|
+
# Therefore, the following tuple type definitions contain a lot of repetitions:
|
|
89
|
+
|
|
90
|
+
Composable1: TypeAlias = tuple[Callable[[_T0], _T1],]
|
|
91
|
+
|
|
92
|
+
Composable2: TypeAlias = tuple[
|
|
93
|
+
Callable[[_T0], _T1],
|
|
94
|
+
Callable[[_T1], _T2],
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
Composable3: TypeAlias = tuple[
|
|
98
|
+
Callable[[_T0], _T1],
|
|
99
|
+
Callable[[_T1], _T2],
|
|
100
|
+
Callable[[_T2], _T3],
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
Composable4: TypeAlias = tuple[
|
|
104
|
+
Callable[[_T0], _T1],
|
|
105
|
+
Callable[[_T1], _T2],
|
|
106
|
+
Callable[[_T2], _T3],
|
|
107
|
+
Callable[[_T3], _T4],
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
Composable5: TypeAlias = tuple[
|
|
111
|
+
Callable[[_T0], _T1],
|
|
112
|
+
Callable[[_T1], _T2],
|
|
113
|
+
Callable[[_T2], _T3],
|
|
114
|
+
Callable[[_T3], _T4],
|
|
115
|
+
Callable[[_T4], _T5],
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
Composable6: TypeAlias = tuple[
|
|
119
|
+
Callable[[_T0], _T1],
|
|
120
|
+
Callable[[_T1], _T2],
|
|
121
|
+
Callable[[_T2], _T3],
|
|
122
|
+
Callable[[_T3], _T4],
|
|
123
|
+
Callable[[_T4], _T5],
|
|
124
|
+
Callable[[_T5], _T6],
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
Composable7: TypeAlias = tuple[
|
|
128
|
+
Callable[[_T0], _T1],
|
|
129
|
+
Callable[[_T1], _T2],
|
|
130
|
+
Callable[[_T2], _T3],
|
|
131
|
+
Callable[[_T3], _T4],
|
|
132
|
+
Callable[[_T4], _T5],
|
|
133
|
+
Callable[[_T5], _T6],
|
|
134
|
+
Callable[[_T6], _T7],
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
Composable: TypeAlias = Union[
|
|
138
|
+
Composable7[_IN, _T1, _T2, _T3, _T4, _T5, _T6, _OUT],
|
|
139
|
+
Composable6[_IN, _T1, _T2, _T3, _T4, _T5, _OUT],
|
|
140
|
+
Composable5[_IN, _T1, _T2, _T3, _T4, _OUT],
|
|
141
|
+
Composable4[_IN, _T1, _T2, _T3, _OUT],
|
|
142
|
+
Composable3[_IN, _T1, _T2, _OUT],
|
|
143
|
+
Composable2[_IN, _T1, _OUT],
|
|
144
|
+
Composable1[_IN, _OUT],
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
Pipeline0: TypeAlias = tuple[_T0,]
|
|
148
|
+
|
|
149
|
+
Pipeline1: TypeAlias = tuple[
|
|
150
|
+
_T0,
|
|
151
|
+
Callable[[_T0], _T1],
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
Pipeline2: TypeAlias = tuple[
|
|
155
|
+
_T0,
|
|
156
|
+
Callable[[_T0], _T1],
|
|
157
|
+
Callable[[_T1], _T2],
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
Pipeline3: TypeAlias = tuple[
|
|
161
|
+
_T0,
|
|
162
|
+
Callable[[_T0], _T1],
|
|
163
|
+
Callable[[_T1], _T2],
|
|
164
|
+
Callable[[_T2], _T3],
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
Pipeline4: TypeAlias = tuple[
|
|
168
|
+
_T0,
|
|
169
|
+
Callable[[_T0], _T1],
|
|
170
|
+
Callable[[_T1], _T2],
|
|
171
|
+
Callable[[_T2], _T3],
|
|
172
|
+
Callable[[_T3], _T4],
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
Pipeline5: TypeAlias = tuple[
|
|
176
|
+
_T0,
|
|
177
|
+
Callable[[_T0], _T1],
|
|
178
|
+
Callable[[_T1], _T2],
|
|
179
|
+
Callable[[_T2], _T3],
|
|
180
|
+
Callable[[_T3], _T4],
|
|
181
|
+
Callable[[_T4], _T5],
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
Pipeline6: TypeAlias = tuple[
|
|
185
|
+
_T0,
|
|
186
|
+
Callable[[_T0], _T1],
|
|
187
|
+
Callable[[_T1], _T2],
|
|
188
|
+
Callable[[_T2], _T3],
|
|
189
|
+
Callable[[_T3], _T4],
|
|
190
|
+
Callable[[_T4], _T5],
|
|
191
|
+
Callable[[_T5], _T6],
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
Pipeline7: TypeAlias = tuple[
|
|
195
|
+
_T0,
|
|
196
|
+
Callable[[_T0], _T1],
|
|
197
|
+
Callable[[_T1], _T2],
|
|
198
|
+
Callable[[_T2], _T3],
|
|
199
|
+
Callable[[_T3], _T4],
|
|
200
|
+
Callable[[_T4], _T5],
|
|
201
|
+
Callable[[_T5], _T6],
|
|
202
|
+
Callable[[_T6], _T7],
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
Pipeline: TypeAlias = Union[
|
|
206
|
+
Pipeline7[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _OUT],
|
|
207
|
+
Pipeline6[_T0, _T1, _T2, _T3, _T4, _T5, _OUT],
|
|
208
|
+
Pipeline5[_T0, _T1, _T2, _T3, _T4, _OUT],
|
|
209
|
+
Pipeline4[_T0, _T1, _T2, _T3, _OUT],
|
|
210
|
+
Pipeline3[_T0, _T1, _T2, _OUT],
|
|
211
|
+
Pipeline2[_T0, _T1, _OUT],
|
|
212
|
+
Pipeline1[_T0, _OUT],
|
|
213
|
+
Pipeline0[_OUT],
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def compose( # noqa: PLR0911
|
|
218
|
+
c: Composable[_IN, _T1, _T2, _T3, _T4, _T5, _T6, _OUT],
|
|
219
|
+
) -> Callable[[_IN], _OUT]:
|
|
220
|
+
"""Compose a tuple of compatible functions from first to last.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
c: Compatible functions that can be applied sequentially from first to last.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Function that applies the given functions from first to last.
|
|
227
|
+
|
|
228
|
+
Example:
|
|
229
|
+
>>> get_length_string = compose((len, lambda n: f"Length: {n}"))
|
|
230
|
+
>>> get_length_string("Hello, world!")
|
|
231
|
+
'Length: 13'
|
|
232
|
+
"""
|
|
233
|
+
if len(c) == 1:
|
|
234
|
+
return lambda in_: c[0](in_)
|
|
235
|
+
if len(c) == 2: # noqa: PLR2004
|
|
236
|
+
return lambda in_: c[1](c[0](in_))
|
|
237
|
+
if len(c) == 3: # noqa: PLR2004
|
|
238
|
+
return lambda in_: c[2](c[1](c[0](in_)))
|
|
239
|
+
if len(c) == 4: # noqa: PLR2004
|
|
240
|
+
return lambda in_: c[3](c[2](c[1](c[0](in_))))
|
|
241
|
+
if len(c) == 5: # noqa: PLR2004
|
|
242
|
+
return lambda in_: c[4](c[3](c[2](c[1](c[0](in_)))))
|
|
243
|
+
if len(c) == 6: # noqa: PLR2004
|
|
244
|
+
return lambda in_: c[5](c[4](c[3](c[2](c[1](c[0](in_))))))
|
|
245
|
+
if len(c) == 7: # noqa: PLR2004
|
|
246
|
+
return lambda in_: c[6](c[5](c[4](c[3](c[2](c[1](c[0](in_)))))))
|
|
247
|
+
return assert_never(c) # type: ignore [unreachable] # pragma: no cover
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def pipe(p: Pipeline[_T0, _T1, _T2, _T3, _T4, _T5, _T6, _OUT]) -> _OUT:
|
|
251
|
+
"""Evaluate a `Pipeline`.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
p:
|
|
255
|
+
Single value followed by up to seven compatible functions
|
|
256
|
+
that can be applied sequentially from first to last.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Result of sequentially applying the given functions from first to last
|
|
260
|
+
to the given value.
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
>>> pipe(("Hello, world!", len, lambda n: f"Length: {n}"))
|
|
264
|
+
'Length: 13'
|
|
265
|
+
"""
|
|
266
|
+
if len(p) == 1:
|
|
267
|
+
return p[0]
|
|
268
|
+
return compose(p[1:])(p[0])
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Monadic functions for `collections.abc.Awaitable`.
|
|
2
|
+
|
|
3
|
+
Provides utilities for functional composition of asynchronous functions.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
>>> import asyncio
|
|
7
|
+
>>> from trcks.fp.composition import pipe
|
|
8
|
+
>>> from trcks.fp.monads import awaitable as a
|
|
9
|
+
>>> async def read_from_disk() -> str:
|
|
10
|
+
... await asyncio.sleep(0.001)
|
|
11
|
+
... input_ = "Hello, world!"
|
|
12
|
+
... print(f"Read '{input_}' from disk.")
|
|
13
|
+
... return input_
|
|
14
|
+
...
|
|
15
|
+
>>> def transform(s: str) -> str:
|
|
16
|
+
... return f"Length: {len(s)}"
|
|
17
|
+
...
|
|
18
|
+
>>> async def write_to_disk(output: str) -> None:
|
|
19
|
+
... await asyncio.sleep(0.001)
|
|
20
|
+
... print(f"Wrote '{output}' to disk.")
|
|
21
|
+
...
|
|
22
|
+
>>> async def main() -> None:
|
|
23
|
+
... awaitable_str = read_from_disk()
|
|
24
|
+
... return await pipe(
|
|
25
|
+
... (awaitable_str, a.map_(transform), a.map_to_awaitable(write_to_disk))
|
|
26
|
+
... )
|
|
27
|
+
...
|
|
28
|
+
>>> asyncio.run(main())
|
|
29
|
+
Read 'Hello, world!' from disk.
|
|
30
|
+
Wrote 'Length: 13' to disk.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from typing import TYPE_CHECKING
|
|
36
|
+
|
|
37
|
+
from trcks._typing import TypeVar
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
40
|
+
from collections.abc import Awaitable, Callable
|
|
41
|
+
|
|
42
|
+
__docformat__ = "google"
|
|
43
|
+
|
|
44
|
+
_T = TypeVar("_T")
|
|
45
|
+
_T1 = TypeVar("_T1")
|
|
46
|
+
_T2 = TypeVar("_T2")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _construct(value: _T) -> _T:
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def construct(value: _T) -> Awaitable[_T]:
|
|
54
|
+
"""Create an `Awaitable` from a value.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
value: The value to create the `Awaitable` from.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
The `Awaitable` created from the value.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> import asyncio
|
|
64
|
+
>>> from collections.abc import Awaitable
|
|
65
|
+
>>> from trcks.fp.monads import awaitable as a
|
|
66
|
+
>>> awtbl = a.construct("Hello, world!")
|
|
67
|
+
>>> isinstance(awtbl, Awaitable)
|
|
68
|
+
True
|
|
69
|
+
>>> asyncio.run(a.to_coroutine(awtbl))
|
|
70
|
+
'Hello, world!'
|
|
71
|
+
"""
|
|
72
|
+
return _construct(value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def map_(f: Callable[[_T1], _T2]) -> Callable[[Awaitable[_T1]], Awaitable[_T2]]:
|
|
76
|
+
"""Turn synchronous function into function expecting and returning `Awaitable`.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
f:
|
|
80
|
+
The synchronous function to be transformed into
|
|
81
|
+
a function expecting and returning an `Awaitable`.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
The given function transformed into
|
|
85
|
+
a function expecting and returning an `Awaitable`.
|
|
86
|
+
|
|
87
|
+
Note:
|
|
88
|
+
The underscore in the function name helps to avoid collisions
|
|
89
|
+
with the built-in function `map`.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
>>> import asyncio
|
|
93
|
+
>>> from collections.abc import Awaitable
|
|
94
|
+
>>> from trcks.fp.monads import awaitable as a
|
|
95
|
+
>>> def transform(s: str) -> str:
|
|
96
|
+
... return f"Length: {len(s)}"
|
|
97
|
+
...
|
|
98
|
+
>>> transform_mapped = a.map_(transform)
|
|
99
|
+
>>> awaitable_input = a.construct("Hello, world!")
|
|
100
|
+
>>> awaitable_output = transform_mapped(awaitable_input)
|
|
101
|
+
>>> isinstance(awaitable_output, Awaitable)
|
|
102
|
+
True
|
|
103
|
+
>>> asyncio.run(a.to_coroutine(awaitable_output))
|
|
104
|
+
'Length: 13'
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def composed_f(value: _T1) -> Awaitable[_T2]:
|
|
109
|
+
return construct(f(value))
|
|
110
|
+
|
|
111
|
+
return map_to_awaitable(composed_f)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def map_to_awaitable(
|
|
115
|
+
f: Callable[[_T1], Awaitable[_T2]],
|
|
116
|
+
) -> Callable[[Awaitable[_T1]], Awaitable[_T2]]:
|
|
117
|
+
"""Turn `Awaitable`-returning func. into func. expecting and returning `Awaitable`.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
f:
|
|
121
|
+
The `Awaitable`-returning function to be transformed into
|
|
122
|
+
a function expecting and returning an `Awaitable`.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The given function transformed into
|
|
126
|
+
a function expecting and returning an `Awaitable`.
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> import asyncio
|
|
131
|
+
>>> from collections.abc import Awaitable
|
|
132
|
+
>>> from trcks.fp.monads import awaitable as a
|
|
133
|
+
>>> async def write_to_disk(output: str) -> None:
|
|
134
|
+
... await asyncio.sleep(0.001)
|
|
135
|
+
... print(f"Wrote '{output}' to disk.")
|
|
136
|
+
...
|
|
137
|
+
>>> write_to_disk_mapped = a.map_to_awaitable(write_to_disk)
|
|
138
|
+
>>> awaitable_input = a.construct("Hello, world!")
|
|
139
|
+
>>> awaitable_output = write_to_disk_mapped(awaitable_input)
|
|
140
|
+
>>> isinstance(awaitable_output, Awaitable)
|
|
141
|
+
True
|
|
142
|
+
>>> asyncio.run(a.to_coroutine(awaitable_output))
|
|
143
|
+
Wrote 'Hello, world!' to disk.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
async def mapped_f(awaitable: Awaitable[_T1]) -> _T2:
|
|
147
|
+
return await f(await awaitable)
|
|
148
|
+
|
|
149
|
+
return mapped_f
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def to_coroutine(awtbl: Awaitable[_T]) -> _T:
|
|
153
|
+
"""Turn an `Awaitable` into a `collections.abc.Coroutine`.
|
|
154
|
+
|
|
155
|
+
This is useful for functions that expect a coroutine (e.g. `asyncio.run`).
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
awtbl: The `Awaitable` to be transformed into a `collections.abc.Coroutine`.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
The given `Awaitable` transformed into a `collections.abc.Coroutine`.
|
|
162
|
+
|
|
163
|
+
Note:
|
|
164
|
+
The type `Awaitable` is a supertype of `collections.abc.Coroutine`.
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
Transform an `asyncio.Future` into a `collections.abc.Coroutine` and run it:
|
|
168
|
+
|
|
169
|
+
>>> import asyncio
|
|
170
|
+
>>> from trcks.fp.monads import awaitable as a
|
|
171
|
+
>>> asyncio.set_event_loop(asyncio.new_event_loop())
|
|
172
|
+
>>> future = asyncio.Future[str]()
|
|
173
|
+
>>> future.set_result("Hello, world!")
|
|
174
|
+
>>> future
|
|
175
|
+
<Future finished result='Hello, world!'>
|
|
176
|
+
>>> coro = a.to_coroutine(future)
|
|
177
|
+
>>> coro
|
|
178
|
+
<coroutine object to_coroutine at 0x...>
|
|
179
|
+
>>> asyncio.run(coro)
|
|
180
|
+
'Hello, world!'
|
|
181
|
+
"""
|
|
182
|
+
return await awtbl
|