pythonic-fp-fptools 5.0.0__tar.gz → 5.1.1__tar.gz

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.
Files changed (30) hide show
  1. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/.gitignore +1 -0
  2. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/CHANGELOG.rst +8 -1
  3. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/PKG-INFO +12 -11
  4. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/README.rst +7 -7
  5. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/pyproject.toml +23 -16
  6. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/src/pythonic_fp/fptools/__init__.py +3 -1
  7. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/src/pythonic_fp/fptools/either.py +93 -61
  8. pythonic_fp_fptools-5.1.1/src/pythonic_fp/fptools/either.pyi +36 -0
  9. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/src/pythonic_fp/fptools/function.py +34 -12
  10. pythonic_fp_fptools-5.1.1/src/pythonic_fp/fptools/function.pyi +11 -0
  11. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/src/pythonic_fp/fptools/lazy.py +58 -48
  12. pythonic_fp_fptools-5.1.1/src/pythonic_fp/fptools/lazy.pyi +18 -0
  13. pythonic_fp_fptools-5.1.1/src/pythonic_fp/fptools/maybe.py +138 -0
  14. pythonic_fp_fptools-5.1.1/src/pythonic_fp/fptools/maybe.pyi +25 -0
  15. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/src/pythonic_fp/fptools/state.py +17 -52
  16. pythonic_fp_fptools-5.1.1/src/pythonic_fp/fptools/state.pyi +23 -0
  17. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/tests/either/test_either.py +0 -2
  18. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/tests/either/test_sequence_either.py +1 -3
  19. pythonic_fp_fptools-5.1.1/tests/either/test_str_repr_either.py +74 -0
  20. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/tests/function/test_function.py +9 -7
  21. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/tests/lazy/test_lazy.py +0 -2
  22. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/tests/maybe/maybe.py +1 -2
  23. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/tests/maybe/test_sequence_maybe.py +4 -6
  24. pythonic_fp_fptools-5.0.0/tests/either/test_str_repr_either.py → pythonic_fp_fptools-5.1.1/tests/maybe/test_str_repr_maybe.py +0 -60
  25. pythonic_fp_fptools-5.0.0/src/pythonic_fp/fptools/maybe.py +0 -141
  26. pythonic_fp_fptools-5.0.0/tests/bool/test_bool.py +0 -195
  27. pythonic_fp_fptools-5.0.0/tests/maybe/test_str_repr_maybe.py +0 -194
  28. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/LICENSE +0 -0
  29. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/src/pythonic_fp/fptools/py.typed +0 -0
  30. {pythonic_fp_fptools-5.0.0 → pythonic_fp_fptools-5.1.1}/tests/state/test_state.py +0 -0
@@ -1,6 +1,7 @@
1
1
  # Minimal version - only add to when necessary
2
2
  **/__pycache__/
3
3
  dist/
4
+ **/.dmypy.json
4
5
  **/.mypy_cache/
5
6
  **/.pytest_cache/
6
7
  **/.ruff_cache
@@ -17,7 +17,14 @@ See `Semantic Versioning 2.0.0 <https://semver.org>`_.
17
17
  Releases and Important Milestones
18
18
  ---------------------------------
19
19
 
20
- 5.0.0 - TBD
20
+ 5.1.0 - 2025-09-09
21
+ ~~~~~~~~~~~~~~~~~~
22
+
23
+ Updated docstrings for new Sphinx docs structure. Probably just a PATCH release,
24
+ made it a MINOR release due to introducing .pyi files.
25
+
26
+
27
+ 5.0.0 - 2025-08-02
21
28
  ~~~~~~~~~~~~~~~~~~
22
29
 
23
30
  Coordinated entire project pythonic-fp PyPI deployment.
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonic-fp-fptools
3
- Version: 5.0.0
3
+ Version: 5.1.1
4
4
  Summary: Pythonic FP - Functional Programming Tools
5
5
  Keywords: either,fp,functional,functional programming,lazy,maybe,monad,non-strict
6
6
  Author-email: "Geoffrey R. Scheller" <geoffrey@scheller.com>
7
7
  Requires-Python: >=3.12
8
8
  Description-Content-Type: text/x-rst
9
- Classifier: Development Status :: 4 - Beta
9
+ Classifier: Development Status :: 3 - Alpha
10
10
  Classifier: Framework :: Pytest
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: Apache Software License
@@ -14,8 +14,9 @@ Classifier: Operating System :: OS Independent
14
14
  Classifier: Programming Language :: Python :: 3.13
15
15
  Classifier: Typing :: Typed
16
16
  License-File: LICENSE
17
- Requires-Dist: pythonic-fp-circulararray>=5.3.0
18
- Requires-Dist: pythonic-fp-singletons>=1.0.0
17
+ Requires-Dist: pythonic-fp-booleans>=1.1.1
18
+ Requires-Dist: pythonic-fp-circulararray>=5.3.2
19
+ Requires-Dist: pythonic-fp-sentinels>=2.1.0
19
20
  Requires-Dist: pytest>=8.4.1 ; extra == "test"
20
21
  Requires-Dist: pythonic-fp-containers>=3.0.0 ; extra == "test"
21
22
  Project-URL: Changelog, https://github.com/grscheller/pythonic-fp-fptools/blob/main/CHANGELOG.rst
@@ -31,13 +32,16 @@ PyPI project
31
32
  `pythonic-fp.fptools
32
33
  <https://pypi.org/project/pythonic-fp.fptools>`_.
33
34
 
34
- Tools to aid with functional programming in Python yet still endeavoring to
35
- remain Pythonic.
35
+ Tools to aid with functional programming in Python while still
36
+ endeavoring to be Pythonic.
36
37
 
37
38
  - Functions as first class objects
38
39
  - Lazy (non-strict) function evaluation
39
40
  - Maybe monad - representing a possible missing value
40
- - Either monad - left bias either monad representing either a LEFT or RIGHT value
41
+ - Either monad - representing either a LEFT or RIGHT value, not both
42
+
43
+ - left biased
44
+
41
45
  - State monad implementation
42
46
 
43
47
  - pure FP handling of state (the state monad)
@@ -45,13 +49,10 @@ remain Pythonic.
45
49
 
46
50
  - the monad encapsulates a state transformation, not a "state"
47
51
 
48
- This PyPI project is part of of the grscheller
52
+ This PyPI project is part of the
49
53
  `pythonic-fp namespace projects
50
54
  <https://github.com/grscheller/pythonic-fp/blob/main/README.md>`_
51
55
 
52
- **Warning:** The maintainer intends to break out the first, forth and
53
- fifth modules to their own repos sometime in the near future.
54
-
55
56
  Documentation
56
57
  -------------
57
58
 
@@ -5,13 +5,16 @@ PyPI project
5
5
  `pythonic-fp.fptools
6
6
  <https://pypi.org/project/pythonic-fp.fptools>`_.
7
7
 
8
- Tools to aid with functional programming in Python yet still endeavoring to
9
- remain Pythonic.
8
+ Tools to aid with functional programming in Python while still
9
+ endeavoring to be Pythonic.
10
10
 
11
11
  - Functions as first class objects
12
12
  - Lazy (non-strict) function evaluation
13
13
  - Maybe monad - representing a possible missing value
14
- - Either monad - left bias either monad representing either a LEFT or RIGHT value
14
+ - Either monad - representing either a LEFT or RIGHT value, not both
15
+
16
+ - left biased
17
+
15
18
  - State monad implementation
16
19
 
17
20
  - pure FP handling of state (the state monad)
@@ -19,13 +22,10 @@ remain Pythonic.
19
22
 
20
23
  - the monad encapsulates a state transformation, not a "state"
21
24
 
22
- This PyPI project is part of of the grscheller
25
+ This PyPI project is part of the
23
26
  `pythonic-fp namespace projects
24
27
  <https://github.com/grscheller/pythonic-fp/blob/main/README.md>`_
25
28
 
26
- **Warning:** The maintainer intends to break out the first, forth and
27
- fifth modules to their own repos sometime in the near future.
28
-
29
29
  Documentation
30
30
  -------------
31
31
 
@@ -1,10 +1,6 @@
1
- [build-system]
2
- requires = ["flit_core>=3.12,<4"]
3
- build-backend = "flit_core.buildapi"
4
-
5
1
  [project]
6
2
  name = "pythonic-fp-fptools"
7
- version = "5.0.0"
3
+ version = "5.1.1"
8
4
  readme = "README.rst"
9
5
  requires-python = ">=3.12"
10
6
  license = { file = "LICENSE" }
@@ -20,7 +16,7 @@ keywords = [
20
16
  "non-strict",
21
17
  ]
22
18
  classifiers = [
23
- "Development Status :: 4 - Beta",
19
+ "Development Status :: 3 - Alpha",
24
20
  "Framework :: Pytest",
25
21
  "Intended Audience :: Developers",
26
22
  "License :: OSI Approved :: Apache Software License",
@@ -29,30 +25,37 @@ classifiers = [
29
25
  "Typing :: Typed",
30
26
  ]
31
27
  dependencies = [
32
- "pythonic-fp-circulararray>=5.3.0",
33
- "pythonic-fp-singletons>=1.0.0",
28
+ "pythonic-fp-booleans>=1.1.1",
29
+ "pythonic-fp-circulararray>=5.3.2",
30
+ "pythonic-fp-sentinels>=2.1.0",
34
31
  ]
35
32
  dynamic = ["description"]
36
33
 
34
+ [project.urls]
35
+ Changelog = "https://github.com/grscheller/pythonic-fp-fptools/blob/main/CHANGELOG.rst"
36
+ Documentation = "https://grscheller.github.io/pythonic-fp/fptools/development/build/html/releases.html"
37
+ Homepage = "https://github.com/grscheller/pythonic-fp/blob/main/README.md"
38
+ Source = "https://github.com/grscheller/pythonic-fp-fptools"
39
+
37
40
  [project.optional-dependencies]
38
41
  test = [
39
42
  "pytest>=8.4.1",
40
43
  "pythonic-fp-containers>=3.0.0",
41
44
  ]
42
45
 
43
- [project.urls]
44
- Changelog = "https://github.com/grscheller/pythonic-fp-fptools/blob/main/CHANGELOG.rst"
45
- Documentation = "https://grscheller.github.io/pythonic-fp/fptools/development/build/html/releases.html"
46
- Homepage = "https://github.com/grscheller/pythonic-fp/blob/main/README.md"
47
- Source = "https://github.com/grscheller/pythonic-fp-fptools"
46
+ [build-system]
47
+ requires = ["flit_core>=3.12,<4"]
48
+ build-backend = "flit_core.buildapi"
48
49
 
49
50
  [tool.flit.module]
50
51
  name = "pythonic_fp.fptools"
51
52
 
52
53
  [tool.mypy]
53
- enable_incomplete_feature = ["NewGenericSyntax"]
54
+ enable_incomplete_feature = "NewGenericSyntax"
55
+ explicit_package_bases = true
54
56
  implicit_reexport = false
55
57
  local_partial_types = true
58
+ namespace_packages = true
56
59
  warn_redundant_casts = true
57
60
  warn_return_any = true
58
61
  warn_unused_configs = true
@@ -60,21 +63,25 @@ warn_unused_configs = true
60
63
  [tool.pylsp-mypy]
61
64
  enabled = true
62
65
  live-mode = true
66
+ dmypy = false
63
67
  strict = true
64
68
  report_progress = true
65
69
 
66
70
  [tool.pytest.ini_options]
71
+ addopts = "-ra"
67
72
  consider_namespace_packages = true
68
73
  testpaths = ["tests/"]
69
- addopts = "-ra"
70
74
 
71
75
  [tool.ruff]
72
76
  target-version = "py313"
73
- ignore = ["E741"]
74
77
 
75
78
  [tool.ruff.lint.flake8-quotes]
76
79
  docstring-quotes = "double"
77
80
 
81
+ [tool.ruff.lint.per-file-ignores]
82
+ "tests/*" = ["C901"]
83
+ "**/*.py" = ["E741"]
84
+
78
85
  [tool.ruff.format]
79
86
  quote-style = "single"
80
87
  docstring-code-line-length = 72
@@ -12,7 +12,9 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- """Pythonic FP - Functional Programming Tools
15
+ """
16
+ Pythonic FP - Functional Programming Tools
17
+ ==========================================
16
18
 
17
19
  Functions as first class objects, Lazy (non-strict) function evaluation,
18
20
  and classical Functional Programming data structures.
@@ -12,46 +12,52 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from __future__ import annotations
16
-
17
15
  __all__ = ['Either', 'LEFT', 'RIGHT']
18
16
 
19
17
  from collections.abc import Callable, Iterator, Sequence
20
- from typing import cast, Never, overload, TypeVar
21
- from pythonic_fp.singletons.sbool import SBool, Truth as Left, Lie as Right
22
- from pythonic_fp.singletons.sentinel import Sentinel as _Sentinel
18
+ from typing import cast, overload
19
+ from pythonic_fp.booleans.subtypable import SBool
23
20
  from .maybe import MayBe
24
21
 
25
- L = TypeVar('L', covariant=True)
26
- R = TypeVar('R', covariant=True)
27
22
 
28
- LEFT = Left('LEFT')
29
- RIGHT = Right('RIGHT')
23
+ class EitherBool(SBool):
24
+ def __repr__(self) -> str:
25
+ if self:
26
+ return 'LEFT'
27
+ return 'RIGHT'
28
+
29
+
30
+ LEFT = EitherBool(True)
31
+ RIGHT = EitherBool(False)
30
32
 
31
33
 
32
34
  class Either[L, R]:
33
- """Either monad, data structure semantically containing either a left
35
+ """
36
+ Either Monad
37
+ ------------
38
+
39
+ Data structure semantically containing either a left
34
40
  or a right value, but not both.
35
41
 
36
42
  Implements a left biased Either Monad.
37
43
 
38
- - `Either(value: +L, LEFT)` produces a left `Either`
39
- - `Either(value: +L, RIGHT)` produces a right `Either`
44
+ - ``Either(value: +L, LEFT)`` produces a left ``Either``
45
+ - ``Either(value: +L, RIGHT)`` produces a right ``Either``
40
46
 
41
47
  In a Boolean context
42
48
 
43
- - `True` if a left `Either`
44
- - `False` if a right `Either`
49
+ - A left ``Either`` is "truthy"
50
+ - A right ``Either`` is "falsy"
45
51
 
46
- Two `Either` objects compare as equal when
52
+ Two ``Either`` objects compare as equal when
47
53
 
48
54
  - both are left values or both are right values whose values
49
55
 
50
56
  - are the same object
51
57
  - compare as equal
52
58
 
53
- Immutable, an `Either` does not change after being created. Therefore
54
- map & bind return new instances
59
+ Immutable, an ``Either`` does not change after being created.
60
+ Therefore ``map`` & ``bind`` return new instances.
55
61
 
56
62
  .. warning::
57
63
 
@@ -60,31 +66,36 @@ class Either[L, R]:
60
66
 
61
67
  .. note::
62
68
 
63
- ``Either(value: +L, side: Left): Either[+L, +R] -> left: Either[+L, +R]``
64
- ``Either(value: +R, side: Right): Either[+L, +R] -> right: Either[+L, +R]``
69
+ ``Either(value: +L, side: Left): Either[L, R] -> left: Either[L, R]``
70
+ ``Either(value: +R, side: Right): Either[L, R] -> right: Either[L, R]``
65
71
 
66
72
  """
73
+
67
74
  __slots__ = '_value', '_side'
68
75
  __match_args__ = ('_value', '_side')
69
76
 
70
- U = TypeVar('U', covariant=True)
71
- V = TypeVar('V', covariant=True)
72
- T = TypeVar('T')
73
-
74
77
  @overload
75
- def __init__(self, value: L, side: Left) -> None: ...
78
+ def __init__(self, value: L) -> None: ...
76
79
  @overload
77
- def __init__(self, value: R, side: Right) -> None: ...
78
-
79
- def __init__(self, value: L | R, side: SBool = LEFT) -> None:
80
- self._value = value
81
- self._side = side
80
+ def __init__(self, value: L, side: EitherBool) -> None: ...
81
+ @overload
82
+ def __init__(self, value: R, side: EitherBool) -> None: ...
83
+
84
+ def __init__(self, value: L | R, side: EitherBool = LEFT) -> None:
85
+ self._value: L | R
86
+ self._side: EitherBool
87
+ if side:
88
+ self._value = value
89
+ self._side = LEFT
90
+ else:
91
+ self._value = value
92
+ self._side = RIGHT
82
93
 
83
94
  def __hash__(self) -> int:
84
- return hash((_Sentinel('XOR'), self._value, self._side))
95
+ return hash((self, self._value, self._side))
85
96
 
86
97
  def __bool__(self) -> bool:
87
- return self._side == LEFT
98
+ return self._side is LEFT
88
99
 
89
100
  def __iter__(self) -> Iterator[L]:
90
101
  if self:
@@ -118,7 +129,7 @@ class Either[L, R]:
118
129
 
119
130
  return False
120
131
 
121
- def get(self) -> L | Never:
132
+ def get(self) -> L:
122
133
  """Get value if a left.
123
134
 
124
135
  .. warning::
@@ -127,8 +138,7 @@ class Either[L, R]:
127
138
  is a right. Best practice is to first check the ``Either`` in
128
139
  a boolean context.
129
140
 
130
- :return: its value if a Left
131
- :rtype: +L
141
+ :returns: its value if a Left
132
142
  :raises ValueError: if not a left
133
143
 
134
144
  """
@@ -138,10 +148,9 @@ class Either[L, R]:
138
148
  return cast(L, self._value)
139
149
 
140
150
  def get_left(self) -> MayBe[L]:
141
- """Get value of `Either` if a left. Safer version of `get` method.
151
+ """Get value of ``Either`` if a left. Safer version of ``get`` method.
142
152
 
143
- - if `Either` contains a left value, return it wrapped in a MayBe
144
- - if `Either` contains a right value, return MayBe()
153
+ :returns: MayBe[L]
145
154
 
146
155
  """
147
156
  if self._side == LEFT:
@@ -149,47 +158,65 @@ class Either[L, R]:
149
158
  return MayBe()
150
159
 
151
160
  def get_right(self) -> MayBe[R]:
152
- """Get value of `Either` if a right
161
+ """Get value of ``Either`` if a right.
153
162
 
154
- - if `Either` contains a right value, return it wrapped in a MayBe
155
- - if `Either` contains a left value, return MayBe()
163
+ :returns: MayBe[R]
156
164
 
157
165
  """
158
166
  if self._side == RIGHT:
159
167
  return MayBe(cast(R, self._value))
160
168
  return MayBe()
161
169
 
162
- def map_right[V](self, f: Callable[[R], V]) -> Either[L, V]:
163
- """Construct new Either with a different right."""
170
+ def map_right[V](self, f: Callable[[R], V]) -> 'Either[L, V]':
171
+ """Construct new Either with a different right.
172
+
173
+ :param f: function to map a right value
174
+ :returns: a new Either if a right, otherwise itself
175
+
176
+ """
164
177
  if self._side == LEFT:
165
178
  return cast(Either[L, V], self)
166
179
  return Either[L, V](f(cast(R, self._value)), RIGHT)
167
180
 
168
- def map[U](self, f: Callable[[L], U]) -> Either[U, R]:
169
- """Map over if a left value. Return new instance."""
181
+ def map[U](self, f: Callable[[L], U]) -> 'Either[U, R]':
182
+ """Map over if a left value. Return new instance.
183
+
184
+ :param f: function to map a left value
185
+ :returns: a new Either if a left, otherwise itself
186
+
187
+ """
170
188
  if self._side == RIGHT:
171
189
  return cast(Either[U, R], self)
172
190
  return Either(f(cast(L, self._value)), LEFT)
173
191
 
174
- def bind[U](self, f: Callable[[L], Either[U, R]]) -> Either[U, R]:
175
- """Flatmap over the left value, propagate right values."""
192
+ def bind[U](self, f: 'Callable[[L], Either[U, R]]') -> 'Either[U, R]':
193
+ """Flatmap over the left value, propagate right values.
194
+
195
+ :param f: function to flatmap a left value
196
+ :returns: a new Either if a left, otherwise itself
197
+
198
+ """
176
199
  if self:
177
200
  return f(cast(L, self._value))
178
201
  return cast(Either[U, R], self)
179
202
 
180
- def map_except[U](self, f: Callable[[L], U], fallback_right: R) -> Either[U, R]:
203
+ def map_except[U](self, f: Callable[[L], U], fallback_right: R) -> 'Either[U, R]':
181
204
  """Map over if a left value - with fallback upon exception.
182
205
 
183
- - if `Either` is a left then map `f` over its value
206
+ - if ``Either`` is a left then map ``f`` over its value
207
+
208
+ - if ``f`` returns normally, then return a left ``Either[U, R]``
209
+ - if ``f`` raises an exception, return right ``Either[U, R]``
184
210
 
185
- - if `f` successful return a left `Either[+U, +R]`
186
- - if `f` unsuccessful return right `Either[+U, +R]`
211
+ - if ``Either`` is a right
187
212
 
188
- - swallows many exceptions `f` may throw at run time
213
+ - return new ``Either(right=self._right): Either[+U, +R]``
189
214
 
190
- - if `Either` is a right
215
+ .. warning::
216
+ Swallows exceptions.
191
217
 
192
- - return new `Either(right=self._right): Either[+U, +R]`
218
+ .. note
219
+ The fallback type must be the same type as the ``Either``
193
220
 
194
221
  """
195
222
  if self._side == RIGHT:
@@ -216,14 +243,19 @@ class Either[L, R]:
216
243
  return applied.get()
217
244
 
218
245
  def bind_except[U](
219
- self, f: Callable[[L], Either[U, R]], fallback_right: R
220
- ) -> Either[U, R]:
221
- """Flatmap `Either` with function `f` with fallback right
246
+ self, f: 'Callable[[L], Either[U, R]]', fallback_right: R
247
+ ) -> 'Either[U, R]':
248
+ """Flatmap ``Either`` with function ``f`` with a fallback right
249
+ if exception is thrown.
222
250
 
223
251
  .. warning::
224
252
  Swallows exceptions.
225
253
 
254
+ .. note
255
+ The fallback type must be the same type as the ``Either``.
256
+
226
257
  :param fallback_right: fallback value if exception thrown
258
+ :returns: a successfully mapped left, a propagated right, or a right with fallback value
227
259
 
228
260
  """
229
261
  if self._side == RIGHT:
@@ -252,12 +284,12 @@ class Either[L, R]:
252
284
 
253
285
  @staticmethod
254
286
  def sequence[U, V](
255
- sequence_xor_uv: Sequence[Either[U, V]],
256
- ) -> Either[Sequence[U], V]:
257
- """Sequence an indexable of type `Either[~U, ~V]`
287
+ sequence_xor_uv: 'Sequence[Either[U, V]]',
288
+ ) -> 'Either[Sequence[U], V]':
289
+ """Sequence a sequence subtype of Sequence[Either[U, V]]``
258
290
 
259
- If the iterated `Either` values are all lefts, then return an `Either` of
260
- an iterable of the left values. Otherwise return a right Either containing
291
+ If the iterated ``Either`` values are all lefts, then return an ``Either`` of
292
+ an iterable of the left values. Otherwise return a right ``Either`` containing
261
293
  the first right encountered.
262
294
 
263
295
  """
@@ -0,0 +1,36 @@
1
+ from .maybe import MayBe
2
+ from _typeshed import Incomplete
3
+ from collections.abc import Callable, Iterator, Sequence
4
+ from pythonic_fp.booleans.subtypable import SBool
5
+ from typing import overload
6
+
7
+ __all__ = ['Either', 'LEFT', 'RIGHT']
8
+
9
+ class EitherBool(SBool): ...
10
+
11
+ LEFT: Incomplete
12
+ RIGHT: Incomplete
13
+
14
+ class Either[L, R]:
15
+ __match_args__: Incomplete
16
+ @overload
17
+ def __init__(self, value: L) -> None: ...
18
+ @overload
19
+ def __init__(self, value: L, side: EitherBool) -> None: ...
20
+ @overload
21
+ def __init__(self, value: R, side: EitherBool) -> None: ...
22
+ def __hash__(self) -> int: ...
23
+ def __bool__(self) -> bool: ...
24
+ def __iter__(self) -> Iterator[L]: ...
25
+ def __len__(self) -> int: ...
26
+ def __eq__(self, other: object) -> bool: ...
27
+ def get(self) -> L: ...
28
+ def get_left(self) -> MayBe[L]: ...
29
+ def get_right(self) -> MayBe[R]: ...
30
+ def map_right[V](self, f: Callable[[R], V]) -> Either[L, V]: ...
31
+ def map[U](self, f: Callable[[L], U]) -> Either[U, R]: ...
32
+ def bind[U](self, f: Callable[[L], Either[U, R]]) -> Either[U, R]: ...
33
+ def map_except[U](self, f: Callable[[L], U], fallback_right: R) -> Either[U, R]: ...
34
+ def bind_except[U](self, f: Callable[[L], Either[U, R]], fallback_right: R) -> Either[U, R]: ...
35
+ @staticmethod
36
+ def sequence[U, V](sequence_xor_uv: Sequence[Either[U, V]]) -> Either[Sequence[U], V]: ...
@@ -12,11 +12,9 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- """Pythonic FP - FP tools for functions
16
-
17
- Not a replacement for the std library's `functools` which is more about
18
- modifying function behavior through decorators than functional composition
19
- and application.
15
+ """
16
+ FP tools for functions
17
+ ======================
20
18
 
21
19
  FP utilities to manipulate and partially apply functions
22
20
 
@@ -27,7 +25,6 @@ FP utilities to manipulate and partially apply functions
27
25
 
28
26
  """
29
27
 
30
- from __future__ import annotations
31
28
  from collections.abc import Callable
32
29
  from typing import Any, ParamSpec
33
30
 
@@ -37,12 +34,29 @@ P = ParamSpec('P')
37
34
 
38
35
 
39
36
  def swap[U, V, R](f: Callable[[U, V], R]) -> Callable[[V, U], R]:
40
- """Swap arguments of a two argument function."""
37
+ """
38
+ Swap args
39
+ ---------
40
+
41
+ Swap arguments of a two argument function.
42
+
43
+ :param f: Two argument function.
44
+ :returns: A version of ``f`` with its arguments swapped.
45
+
46
+ """
41
47
  return lambda v, u: f(u, v)
42
48
 
43
49
 
44
50
  def negate[**P](f: Callable[P, bool]) -> Callable[P, bool]:
45
- """Take a predicate and return its negation."""
51
+ """
52
+ Negate predicate
53
+ ----------------
54
+
55
+ Take a predicate and return its negation.
56
+
57
+ :param f: a function ``f`` which returns a bool
58
+ :returns: the function ``not f``
59
+ """
46
60
 
47
61
  def ff(*args: P.args, **kwargs: P.kwargs) -> bool:
48
62
  return not f(*args, **kwargs)
@@ -51,7 +65,11 @@ def negate[**P](f: Callable[P, bool]) -> Callable[P, bool]:
51
65
 
52
66
 
53
67
  def sequenced[R](f: Callable[..., R]) -> Callable[[tuple[Any]], R]:
54
- """Convert a function with arbitrary positional arguments to one taking
68
+ """
69
+ Multi-to-single valued
70
+ ----------------------
71
+
72
+ Convert a function with arbitrary positional arguments to one taking
55
73
  a tuple of the original arguments.
56
74
 
57
75
  - was awaiting typing and mypy "improvements" to ParamSpec
@@ -59,11 +77,10 @@ def sequenced[R](f: Callable[..., R]) -> Callable[[tuple[Any]], R]:
59
77
  - return type: Callable[tuple[P.args], R] ???
60
78
  - return type: Callable[[tuple[P.args]], R] ???
61
79
 
62
- - not going to happen, `see <https://github.com/python/mypy/pull/18278>`_
63
-
64
80
  TODO: Look into replacing this function with a Callable class?
65
81
 
66
82
  """
83
+
67
84
  def ff(tupled_args: tuple[Any]) -> R:
68
85
  return f(*tupled_args)
69
86
 
@@ -71,12 +88,17 @@ def sequenced[R](f: Callable[..., R]) -> Callable[[tuple[Any]], R]:
71
88
 
72
89
 
73
90
  def partial[**P, R](f: Callable[P, R], *args: Any) -> Callable[..., R]:
74
- """Partially apply arguments to a function, left to right.
91
+ """
92
+ Partial application
93
+ -------------------
94
+
95
+ Partially apply arguments to a function, left to right.
75
96
 
76
97
  - type-wise the only thing guaranteed is the return type
77
98
  - best practice is to cast the result immediately
78
99
 
79
100
  """
101
+
80
102
  def finish(*rest: Any) -> R:
81
103
  return sequenced(f)(args + rest)
82
104
 
@@ -0,0 +1,11 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, ParamSpec
3
+
4
+ __all__ = ['swap', 'sequenced', 'partial', 'negate']
5
+
6
+ P = ParamSpec('P')
7
+
8
+ def swap[U, V, R](f: Callable[[U, V], R]) -> Callable[[V, U], R]: ...
9
+ def negate[**P](f: Callable[P, bool]) -> Callable[P, bool]: ...
10
+ def sequenced[R](f: Callable[..., R]) -> Callable[[tuple[Any]], R]: ...
11
+ def partial[**P, R](f: Callable[P, R], *args: Any) -> Callable[..., R]: ...