pythonic-fp.containers 2.0.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.
@@ -0,0 +1,226 @@
1
+ # Copyright 2023-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Pythonic FP - Maybe Monad"""
16
+
17
+ from __future__ import annotations
18
+
19
+ __all__ = ['MayBe']
20
+
21
+ from collections.abc import Callable, Iterator, Sequence
22
+ from typing import cast, Final, Never, overload, TypeVar
23
+ from pythonic_fp.fptools.singletons import Sentinel
24
+
25
+ D = TypeVar('D', covariant=True)
26
+
27
+ class MayBe[D]:
28
+ """Maybe monad, data structure wrapping a potentially missing value.
29
+
30
+ - where ``MayBe(value)`` contains a possible value of type ``+D``
31
+ - ``MayBe()`` represent a non-existent or missing value of type ``+D``
32
+ - immutable semantics
33
+
34
+ - immutable, therefore made covariant
35
+
36
+ - can store any item of any type, including ``None``
37
+ - can store any value of any type with one exception
38
+
39
+ :: warning:
40
+ hashability invalidated if contained value is not hashable
41
+
42
+ :: note:
43
+ MayBe(): MayBe[+D] -> mb: Xor[+D]
44
+ MayBe(value: +D) -> mb: Xor[+D]
45
+ """
46
+
47
+ U = TypeVar('U', covariant=True)
48
+ V = TypeVar('V', covariant=True)
49
+ T = TypeVar('T')
50
+
51
+ __slots__ = ('_value',)
52
+ __match_args__ = ('_value',)
53
+
54
+ @overload
55
+ def __init__(self) -> None: ...
56
+ @overload
57
+ def __init__(self, value: D) -> None: ...
58
+
59
+ def __init__(self, value: D | Sentinel = Sentinel('MayBe')) -> None:
60
+ self._value: D | Sentinel = value
61
+
62
+ def __hash__(self) -> int:
63
+ return hash((Sentinel('MayBe'), self._value))
64
+
65
+ def __bool__(self) -> bool:
66
+ return self._value is not Sentinel('MayBe')
67
+
68
+ def __iter__(self) -> Iterator[D]:
69
+ if self:
70
+ yield cast(D, self._value)
71
+
72
+ def __repr__(self) -> str:
73
+ if self:
74
+ return 'MayBe(' + repr(self._value) + ')'
75
+ return 'MayBe()'
76
+
77
+ def __len__(self) -> int:
78
+ return 1 if self else 0
79
+
80
+ def __eq__(self, other: object) -> bool:
81
+ if not isinstance(other, type(self)):
82
+ return False
83
+ if self._value is other._value:
84
+ return True
85
+ if self._value == other._value:
86
+ return True
87
+ return False
88
+
89
+ @overload
90
+ def get(self) -> D | Never: ...
91
+ @overload
92
+ def get(self, alt: D) -> D: ...
93
+
94
+ def get(self, alt: D | Sentinel = Sentinel('MayBe')) -> D | Never:
95
+ """Return the contained value if it exists, otherwise an alternate value.
96
+
97
+ :: warning:
98
+ Unsafe method ``get``. Will raise ``ValueError`` if MayBe empty
99
+ and an alt return value not given. Best practice is to first check
100
+ the MayBe in a boolean context.
101
+
102
+ :rtype: +D
103
+ :raises ValueError: when an alternate value is not provided but needed
104
+ """
105
+ _sentinel: Final[Sentinel] = Sentinel('MayBe')
106
+ if self._value is not _sentinel:
107
+ return cast(D, self._value)
108
+ if alt is _sentinel:
109
+ msg = 'MayBe: an alternate return type not provided'
110
+ raise ValueError(msg)
111
+ return cast(D, alt)
112
+
113
+ def map[U](self, f: Callable[[D], U]) -> MayBe[U]:
114
+ """Map function `f` over contents."""
115
+
116
+ if self:
117
+ return MayBe(f(cast(D, self._value)))
118
+ return cast(MayBe[U], self)
119
+
120
+ def bind[U](self, f: Callable[[D], MayBe[U]]) -> MayBe[U]:
121
+ """Flatmap ``MayBe`` with function ``f``."""
122
+ return f(cast(D, self._value)) if self else cast(MayBe[U], self)
123
+
124
+ def map_except[U](self, f: Callable[[D], U]) -> MayBe[U]:
125
+ """Map function ``f`` over contents.
126
+
127
+ If ``f`` should fail, return a ``MayBe()``.
128
+
129
+ :: warning:
130
+ Swallows exceptions
131
+ """
132
+ if not self:
133
+ return cast(MayBe[U], self)
134
+ try:
135
+ return MayBe(f(cast(D, self._value)))
136
+ except (
137
+ LookupError,
138
+ ValueError,
139
+ TypeError,
140
+ BufferError,
141
+ ArithmeticError,
142
+ RecursionError,
143
+ ReferenceError,
144
+ RuntimeError,
145
+ ):
146
+ return MayBe()
147
+
148
+ def bind_except[U](self, f: Callable[[D], MayBe[U]]) -> MayBe[U]:
149
+ """Flatmap `MayBe` with function `f`.
150
+
151
+ :: warning:
152
+ Swallows exceptions
153
+ """
154
+ try:
155
+ return f(cast(D, self._value)) if self else cast(MayBe[U], self)
156
+ except (
157
+ LookupError,
158
+ ValueError,
159
+ TypeError,
160
+ BufferError,
161
+ ArithmeticError,
162
+ RecursionError,
163
+ ReferenceError,
164
+ RuntimeError,
165
+ ):
166
+ return MayBe()
167
+
168
+ @staticmethod
169
+ def sequence[U](sequence_mb_u: Sequence[MayBe[U]]) -> MayBe[Sequence[U]]:
170
+ """Sequence a mutable indexable of type ``MayBe[~U]``
171
+
172
+ If the iterated `MayBe` values are not all empty,
173
+
174
+ - return a `MayBe` of the Sequence subtype of the contained values
175
+ - otherwise return an empty `MayBe`
176
+
177
+ """
178
+ list_items: list[U] = list()
179
+
180
+ for mb_u in sequence_mb_u:
181
+ if mb_u:
182
+ list_items.append(mb_u.get())
183
+ else:
184
+ return MayBe()
185
+
186
+ sequence_type = cast(Sequence[U], type(sequence_mb_u))
187
+
188
+ return MayBe(sequence_type(list_items)) # type: ignore # subclass will be callable
189
+
190
+ @staticmethod
191
+ def failable_call[T, V](f: Callable[[T], V], t: T) -> MayBe[V]:
192
+ """Return MayBe wrapped result of a function call that can fail."""
193
+ try:
194
+ mb_return = MayBe(f(t))
195
+ except (
196
+ LookupError,
197
+ ValueError,
198
+ TypeError,
199
+ BufferError,
200
+ ArithmeticError,
201
+ RecursionError,
202
+ ReferenceError,
203
+ RuntimeError,
204
+ ):
205
+ mb_return = MayBe()
206
+
207
+ return mb_return
208
+
209
+ @staticmethod
210
+ def failable_index[V](vs: Sequence[V], ii: int) -> MayBe[V]:
211
+ """Return a MayBe of an indexed value that can fail."""
212
+ try:
213
+ mb_return = MayBe(vs[ii])
214
+ except (
215
+ LookupError,
216
+ ValueError,
217
+ TypeError,
218
+ BufferError,
219
+ ArithmeticError,
220
+ RecursionError,
221
+ ReferenceError,
222
+ RuntimeError,
223
+ ):
224
+ mb_return = MayBe()
225
+
226
+ return mb_return
File without changes
@@ -0,0 +1,314 @@
1
+ # Copyright 2023-2025 Geoffrey R. Scheller
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Pythonic FP - Either monad
16
+
17
+ - *class* Xor: left biased either monad
18
+
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ __all__ = ['Xor', 'LEFT', 'RIGHT']
24
+
25
+ from collections.abc import Callable, Iterator, Sequence
26
+ from typing import cast, Never, overload, TypeVar
27
+ from pythonic_fp.fptools.bool import Bool as Both, Truth as Left, Lie as Right
28
+ from pythonic_fp.fptools.singletons import Sentinel as _Sentinel
29
+ from .maybe import MayBe
30
+
31
+ L = TypeVar('L', covariant=True)
32
+ R = TypeVar('R', covariant=True)
33
+
34
+ LEFT = Left('LEFT')
35
+ RIGHT = Right('RIGHT')
36
+
37
+
38
+ class Xor[L, R]:
39
+ """Either monad, data structure semantically containing either a left
40
+ or a right value, but not both.
41
+
42
+ Implements a left biased Either Monad.
43
+
44
+ - `Xor(value: +L, LEFT)` produces a left `Xor`
45
+ - `Xor(value: +L, RIGHT)` produces a right `Xor`
46
+
47
+ In a Boolean context
48
+
49
+ - `True` if a left `Xor`
50
+ - `False` if a right `Xor`
51
+
52
+ Two `Xor` objects compare as equal when
53
+
54
+ - both are left values or both are right values whose values
55
+
56
+ - are the same object
57
+ - compare as equal
58
+
59
+ Immutable, an `Xor` does not change after being created.
60
+
61
+ - immutable semantics, map & bind return new instances
62
+
63
+ - warning: contained value need not be immutable
64
+ - warning: not hashable if value is mutable
65
+
66
+ :: Note:
67
+ Xor(value: +L, side: Left): Xor[+L, +R] -> left: Xor[+L, +R]
68
+ Xor(value: +R, side: Right): Xor[+L, +R] -> right: Xor[+L, +R]
69
+ """
70
+
71
+ __slots__ = '_value', '_side'
72
+ __match_args__ = ('_value', '_side')
73
+
74
+ U = TypeVar('U', covariant=True)
75
+ V = TypeVar('V', covariant=True)
76
+ T = TypeVar('T')
77
+
78
+ @overload
79
+ def __init__(self, value: L, side: Left) -> None: ...
80
+ @overload
81
+ def __init__(self, value: R, side: Right) -> None: ...
82
+
83
+ def __init__(self, value: L | R, side: Both = LEFT) -> None:
84
+ self._value = value
85
+ self._side = side
86
+
87
+ def __hash__(self) -> int:
88
+ return hash((_Sentinel('XOR'), self._value, self._side))
89
+
90
+ def __bool__(self) -> bool:
91
+ return self._side == LEFT
92
+
93
+ def __iter__(self) -> Iterator[L]:
94
+ if self:
95
+ yield cast(L, self._value)
96
+
97
+ def __repr__(self) -> str:
98
+ if self:
99
+ return 'Xor(' + repr(self._value) + ', LEFT)'
100
+ return 'Xor(' + repr(self._value) + ', RIGHT)'
101
+
102
+ def __str__(self) -> str:
103
+ if self:
104
+ return '< ' + str(self._value) + ' | >'
105
+ return '< | ' + str(self._value) + ' >'
106
+
107
+ def __len__(self) -> int:
108
+ # An Xor always contains just one value.
109
+ return 1
110
+
111
+ def __eq__(self, other: object) -> bool:
112
+ if not isinstance(other, type(self)):
113
+ return False
114
+
115
+ if self and other:
116
+ if (self._value is other._value) or (self._value == other._value):
117
+ return True
118
+
119
+ if not self and not other:
120
+ if (self._value is other._value) or (self._value == other._value):
121
+ return True
122
+
123
+ return False
124
+
125
+ def get(self) -> L | Never:
126
+ """Get value if a left.
127
+
128
+ :: warning:
129
+ Unsafe method ``get``. Will raise ``ValueError`` if ``Xor``
130
+ is a right. Best practice is to first check the ``Xor`` in
131
+ a boolean context.
132
+
133
+ :return: its value if a Left
134
+ :rtype: +L
135
+ :raises ValueError: if not a left
136
+ """
137
+ if self._side == RIGHT:
138
+ msg = 'Xor: get method called on a right valued Xor'
139
+ raise ValueError(msg)
140
+ return cast(L, self._value)
141
+
142
+ def get_left(self) -> MayBe[L]:
143
+ """Get value of `Xor` if a left. Safer version of `get` method.
144
+
145
+ - if `Xor` contains a left value, return it wrapped in a MayBe
146
+ - if `Xor` contains a right value, return MayBe()
147
+ """
148
+ if self._side == LEFT:
149
+ return MayBe(cast(L, self._value))
150
+ return MayBe()
151
+
152
+ def get_right(self) -> MayBe[R]:
153
+ """Get value of `Xor` if a right
154
+
155
+ - if `Xor` contains a right value, return it wrapped in a MayBe
156
+ - if `Xor` contains a left value, return MayBe()
157
+ """
158
+ if self._side == RIGHT:
159
+ return MayBe(cast(R, self._value))
160
+ return MayBe()
161
+
162
+ def map_right[V](self, f: Callable[[R], V]) -> Xor[L, V]:
163
+ """Construct new Xor with a different right."""
164
+ if self._side == LEFT:
165
+ return cast(Xor[L, V], self)
166
+ return Xor[L, V](f(cast(R, self._value)), RIGHT)
167
+
168
+ def map[U](self, f: Callable[[L], U]) -> Xor[U, R]:
169
+ """Map over if a left value. Return new instance."""
170
+ if self._side == RIGHT:
171
+ return cast(Xor[U, R], self)
172
+ return Xor(f(cast(L, self._value)), LEFT)
173
+
174
+ def bind[U](self, f: Callable[[L], Xor[U, R]]) -> Xor[U, R]:
175
+ """Flatmap over the left value, propagate right values."""
176
+ if self:
177
+ return f(cast(L, self._value))
178
+ return cast(Xor[U, R], self)
179
+
180
+ def map_except[U](self, f: Callable[[L], U], fallback_right: R) -> Xor[U, R]:
181
+ """Map over if a left value - with fallback upon exception.
182
+
183
+ - if `Xor` is a left then map `f` over its value
184
+
185
+ - if `f` successful return a left `Xor[+U, +R]`
186
+ - if `f` unsuccessful return right `Xor[+U, +R]`
187
+
188
+ - swallows many exceptions `f` may throw at run time
189
+
190
+ - if `Xor` is a right
191
+
192
+ - return new `Xor(right=self._right): Xor[+U, +R]`
193
+
194
+ """
195
+ if self._side == RIGHT:
196
+ return cast(Xor[U, R], self)
197
+
198
+ applied: MayBe[Xor[U, R]] = MayBe()
199
+ fall_back: MayBe[Xor[U, R]] = MayBe()
200
+ try:
201
+ applied = MayBe(Xor(f(cast(L, self._value)), LEFT))
202
+ except (
203
+ LookupError,
204
+ ValueError,
205
+ TypeError,
206
+ BufferError,
207
+ ArithmeticError,
208
+ RecursionError,
209
+ ReferenceError,
210
+ RuntimeError,
211
+ ):
212
+ fall_back = MayBe(cast(Xor[U, R], Xor(fallback_right, RIGHT)))
213
+
214
+ if fall_back:
215
+ return fall_back.get()
216
+ return applied.get()
217
+
218
+ def bind_except[U](
219
+ self, f: Callable[[L], Xor[U, R]], fallback_right: R
220
+ ) -> Xor[U, R]:
221
+ """Flatmap `Xor` with function `f` with fallback right
222
+
223
+ :: warning:
224
+ Swallows exceptions.
225
+
226
+ :param fallback_right: fallback value if exception thrown
227
+
228
+ """
229
+ if self._side == RIGHT:
230
+ return cast(Xor[U, R], self)
231
+
232
+ applied: MayBe[Xor[U, R]] = MayBe()
233
+ fall_back: MayBe[Xor[U, R]] = MayBe()
234
+ try:
235
+ if self:
236
+ applied = MayBe(f(cast(L, self._value)))
237
+ except (
238
+ LookupError,
239
+ ValueError,
240
+ TypeError,
241
+ BufferError,
242
+ ArithmeticError,
243
+ RecursionError,
244
+ ReferenceError,
245
+ RuntimeError,
246
+ ):
247
+ fall_back = MayBe(cast(Xor[U, R], Xor(fallback_right, RIGHT)))
248
+
249
+ if fall_back:
250
+ return fall_back.get()
251
+ return applied.get()
252
+
253
+ @staticmethod
254
+ def sequence[U, V](sequence_xor_uv: Sequence[Xor[U, V]]) -> Xor[Sequence[U], V]:
255
+ """Sequence an indexable of type `Xor[~U, ~V]`
256
+
257
+ If the iterated `Xor` values are all lefts, then return an `Xor` of
258
+ an iterable of the left values. Otherwise return a right Xor containing
259
+ the first right encountered.
260
+
261
+ """
262
+ list_items: list[U] = []
263
+
264
+ for xor_uv in sequence_xor_uv:
265
+ if xor_uv:
266
+ list_items.append(xor_uv.get())
267
+ else:
268
+ return Xor(xor_uv.get_right().get(), RIGHT)
269
+
270
+ sequence_type = cast(Sequence[U], type(sequence_xor_uv))
271
+
272
+ return Xor(sequence_type(list_items)) # type: ignore # subclass will be callable
273
+
274
+ @staticmethod
275
+ def failable_call[T, V](f: Callable[[T], V], left: T) -> Xor[V, Exception]:
276
+ """Return Xor wrapped result of a function call that can fail
277
+
278
+ :: warning:
279
+ Swallows exceptions.
280
+ """
281
+ try:
282
+ xor_return = Xor[V, Exception](f(left), LEFT)
283
+ except (
284
+ LookupError,
285
+ ValueError,
286
+ TypeError,
287
+ BufferError,
288
+ ArithmeticError,
289
+ RecursionError,
290
+ ReferenceError,
291
+ RuntimeError,
292
+ ) as exc:
293
+ xor_return = Xor(exc, RIGHT)
294
+
295
+ return xor_return
296
+
297
+ @staticmethod
298
+ def failable_index[V](v: Sequence[V], ii: int) -> Xor[V, Exception]:
299
+ """Return an Xor of an indexed value that can fail.
300
+
301
+ :: warning:
302
+ Swallows exceptions.
303
+ """
304
+ try:
305
+ xor_return = Xor[V, Exception](v[ii], LEFT)
306
+ except (
307
+ IndexError,
308
+ TypeError,
309
+ ArithmeticError,
310
+ RuntimeError,
311
+ ) as exc:
312
+ xor_return = Xor(exc, RIGHT)
313
+
314
+ return xor_return
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythonic-fp.containers
3
+ Version: 2.0.0
4
+ Summary: Developer Tools - Container-like data structures
5
+ Keywords: boxes,functional_tuple,immutable_list,maybe
6
+ Author-email: "Geoffrey R. Scheller" <geoffrey@scheller.com>
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Framework :: Pytest
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Typing :: Typed
16
+ License-File: LICENSE
17
+ Requires-Dist: pythonic-fp.fptools >=3.0.0
18
+ Requires-Dist: pythonic-fp.iterables >=3.0.0
19
+ Project-URL: Changelog, https://grscheller.github.io/pythonic-fp-containers/html/changelog.html
20
+ Project-URL: Documentation, https://grscheller.github.io/pythonic-fp-containers/html/
21
+ Project-URL: Homepage, https://github.com/grscheller/pythonic-fp/blob/main/README.md
22
+ Project-URL: Source, https://github.com/grscheller/pythonic-fp-containers
23
+
24
+ # Pythonic FP - Containers
25
+
26
+ PyPI project
27
+ [pythonic-fp.containers](https://pypi.org/project/pythonic-fp.containers/)
28
+ implements a Python package of container like data structures.
29
+
30
+ | Description | Module |
31
+ |:----------- |:----------- |
32
+ | Single item box | *module* `pythonic_fp.containers.box` |
33
+ | Functional tuple | *module* `pythonic_fp.containers.functional_tuple` |
34
+ | Immutable list | *module* `pythonic_fp.containers.immutable_list` |
35
+ | Maybe monad | *module* `pythonic_fp.containers.maybe` |
36
+ | Either monad | *module* `pythonic_fp.containers.xor` |
37
+
38
+ This PyPI project is part of of the grscheller
39
+ [pythonic-fp namespace projects](https://github.com/grscheller/pythonic-fp/blob/main/README.md).
40
+
41
+ ## Documentation
42
+
43
+ Documentation hosted on
44
+ [GitHub Pages](https://grscheller.github.io/pythonic-fp-containers/html).
45
+
46
+ ## Copyright and License
47
+
48
+ Copyright (c) 2023-2025 Geoffrey R. Scheller. Licensed under the Apache
49
+ License, Version 2.0. See the LICENSE file for details.
50
+
@@ -0,0 +1,11 @@
1
+ pythonic_fp/containers/__init__.py,sha256=LAacANoS0XJptegwYwchEOrYEaOFznEtUG54TGG3G4Y,780
2
+ pythonic_fp/containers/box.py,sha256=mlj1Ua1UKtdtKSChUV43q1_S5kHi0GVcFxBz593YwmE,4723
3
+ pythonic_fp/containers/functional_tuple.py,sha256=hcQXzU9M4OGnvcnbWvB8JpyJBKDwrXOyo9Sr8IGZDxY,6329
4
+ pythonic_fp/containers/immutable_list.py,sha256=cfmevJhofCuUh1FhGIlQMWimgNN-eGbcRgweXjEEWsE,7360
5
+ pythonic_fp/containers/maybe.py,sha256=W2J099F2Cn8PxbEPfiYirQPdD1VynrFuIE7sm9UGOBI,6719
6
+ pythonic_fp/containers/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ pythonic_fp/containers/xor.py,sha256=eKfm5OjFl5EXZRZqErEpf7hIEGQkRsF5r3FpXoCKgJQ,9446
8
+ pythonic_fp_containers-2.0.0.dist-info/licenses/LICENSE,sha256=IJMkVWQUVy-SonK6SMaMotDQ6Zb-lVi2bUTiGp1GUO8,10774
9
+ pythonic_fp_containers-2.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
10
+ pythonic_fp_containers-2.0.0.dist-info/METADATA,sha256=yJqZs_TVSre4fnO8erOHgaTX6TSHJSuBedT6fy8q9Po,2048
11
+ pythonic_fp_containers-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any