funstruct 1.0.0__tar.gz → 1.0.2__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 (102) hide show
  1. funstruct-1.0.2/.github/workflows/docs.yml +31 -0
  2. {funstruct-1.0.0 → funstruct-1.0.2}/PKG-INFO +4 -2
  3. {funstruct-1.0.0 → funstruct-1.0.2}/README.md +1 -0
  4. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/applicative/validated.py +13 -0
  5. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monad/result.py +46 -17
  6. {funstruct-1.0.0 → funstruct-1.0.2}/mkdocs.yml +4 -0
  7. {funstruct-1.0.0 → funstruct-1.0.2}/pyproject.toml +3 -2
  8. {funstruct-1.0.0 → funstruct-1.0.2}/uv.lock +1 -1
  9. funstruct-1.0.2/uv.toml +3 -0
  10. {funstruct-1.0.0 → funstruct-1.0.2}/.claude/settings.local.json +0 -0
  11. {funstruct-1.0.0 → funstruct-1.0.2}/.github/workflows/ci.yml +0 -0
  12. {funstruct-1.0.0 → funstruct-1.0.2}/.github/workflows/publish.yml +0 -0
  13. {funstruct-1.0.0 → funstruct-1.0.2}/.gitignore +0 -0
  14. {funstruct-1.0.0 → funstruct-1.0.2}/.pre-commit-config.yaml +0 -0
  15. {funstruct-1.0.0 → funstruct-1.0.2}/CONTRIBUTING.md +0 -0
  16. {funstruct-1.0.0 → funstruct-1.0.2}/LICENSE +0 -0
  17. {funstruct-1.0.0 → funstruct-1.0.2}/benchmarks/test_bench_collections.py +0 -0
  18. {funstruct-1.0.0 → funstruct-1.0.2}/docs/applicative/index.md +0 -0
  19. {funstruct-1.0.0 → funstruct-1.0.2}/docs/applicative/validated.md +0 -0
  20. {funstruct-1.0.0 → funstruct-1.0.2}/docs/collections/cons.md +0 -0
  21. {funstruct-1.0.0 → funstruct-1.0.2}/docs/collections/frozendict.md +0 -0
  22. {funstruct-1.0.0 → funstruct-1.0.2}/docs/collections/index.md +0 -0
  23. {funstruct-1.0.0 → funstruct-1.0.2}/docs/collections/tree.md +0 -0
  24. {funstruct-1.0.0 → funstruct-1.0.2}/docs/functor/.gitkeep +0 -0
  25. {funstruct-1.0.0 → funstruct-1.0.2}/docs/functor/index.md +0 -0
  26. {funstruct-1.0.0 → funstruct-1.0.2}/docs/index.md +0 -0
  27. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monad/either.md +0 -0
  28. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monad/future.md +0 -0
  29. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monad/index.md +0 -0
  30. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monad/option.md +0 -0
  31. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monad/reader.md +0 -0
  32. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monad/result.md +0 -0
  33. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monad/state.md +0 -0
  34. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monad/writer.md +0 -0
  35. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monadtransformer/either_t.md +0 -0
  36. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monadtransformer/index.md +0 -0
  37. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monadtransformer/option_t.md +0 -0
  38. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monadtransformer/reader_t.md +0 -0
  39. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monadtransformer/state_t.md +0 -0
  40. {funstruct-1.0.0 → funstruct-1.0.2}/docs/monadtransformer/writer_t.md +0 -0
  41. {funstruct-1.0.0 → funstruct-1.0.2}/docs/typeclass/applicative.md +0 -0
  42. {funstruct-1.0.0 → funstruct-1.0.2}/docs/typeclass/index.md +0 -0
  43. {funstruct-1.0.0 → funstruct-1.0.2}/docs/typeclass/monad.md +0 -0
  44. {funstruct-1.0.0 → funstruct-1.0.2}/docs/typeclass/monadtransformer.md +0 -0
  45. {funstruct-1.0.0 → funstruct-1.0.2}/docs/typeclass/monoid.md +0 -0
  46. {funstruct-1.0.0 → funstruct-1.0.2}/docs/typeclass/semigroup.md +0 -0
  47. {funstruct-1.0.0 → funstruct-1.0.2}/docs/util/tailrec.md +0 -0
  48. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/__init__.py +0 -0
  49. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/applicative/__init__.py +0 -0
  50. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/collections/__init__.py +0 -0
  51. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/collections/cons.py +0 -0
  52. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/collections/frozendict.py +0 -0
  53. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/collections/tree.py +0 -0
  54. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/functor/__init__.py +0 -0
  55. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monad/__init__.py +0 -0
  56. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monad/either.py +0 -0
  57. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monad/future.py +0 -0
  58. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monad/option.py +0 -0
  59. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monad/reader.py +0 -0
  60. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monad/state.py +0 -0
  61. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monad/writer.py +0 -0
  62. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monadtransformer/__init__.py +0 -0
  63. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monadtransformer/either_t.py +0 -0
  64. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monadtransformer/option_t.py +0 -0
  65. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monadtransformer/reader_t.py +0 -0
  66. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monadtransformer/state_t.py +0 -0
  67. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monadtransformer/writer_t.py +0 -0
  68. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/monoid/__init__.py +0 -0
  69. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/py.typed +0 -0
  70. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/semigroup/__init__.py +0 -0
  71. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/typeclasses/__init__.py +0 -0
  72. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/typeclasses/_applicative.py +0 -0
  73. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/typeclasses/_functor.py +0 -0
  74. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/typeclasses/_monad.py +0 -0
  75. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/typeclasses/_monad_transformer.py +0 -0
  76. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/typeclasses/_monoid.py +0 -0
  77. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/typeclasses/_semigroup.py +0 -0
  78. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/util/__init__.py +0 -0
  79. {funstruct-1.0.0 → funstruct-1.0.2}/funstruct/util/tailrec.py +0 -0
  80. {funstruct-1.0.0 → funstruct-1.0.2}/justfile +0 -0
  81. {funstruct-1.0.0 → funstruct-1.0.2}/mise.toml +0 -0
  82. {funstruct-1.0.0 → funstruct-1.0.2}/noxfile.py +0 -0
  83. {funstruct-1.0.0 → funstruct-1.0.2}/tests/__init__.py +0 -0
  84. {funstruct-1.0.0 → funstruct-1.0.2}/tests/laws.py +0 -0
  85. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_cons.py +0 -0
  86. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_either.py +0 -0
  87. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_either_t.py +0 -0
  88. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_frozendict.py +0 -0
  89. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_future.py +0 -0
  90. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_law_violations.py +0 -0
  91. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_option.py +0 -0
  92. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_option_t.py +0 -0
  93. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_reader.py +0 -0
  94. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_reader_t.py +0 -0
  95. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_result.py +0 -0
  96. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_state.py +0 -0
  97. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_state_t.py +0 -0
  98. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_tailrec.py +0 -0
  99. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_tree.py +0 -0
  100. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_validated.py +0 -0
  101. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_writer.py +0 -0
  102. {funstruct-1.0.0 → funstruct-1.0.2}/tests/test_writer_t.py +0 -0
@@ -0,0 +1,31 @@
1
+ name: Docs
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ release:
7
+ types: [published]
8
+
9
+ permissions:
10
+ contents: write
11
+
12
+ jobs:
13
+ deploy:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v5
17
+ with:
18
+ fetch-depth: 0
19
+ - uses: astral-sh/setup-uv@v7
20
+ - run: uv sync --group docs
21
+ - name: Deploy docs
22
+ run: |
23
+ git config user.name "github-actions[bot]"
24
+ git config user.email "github-actions[bot]@users.noreply.github.com"
25
+ if [ "${{ github.event_name }}" = "release" ]; then
26
+ VERSION=${{ github.event.release.tag_name }}
27
+ else
28
+ VERSION=dev
29
+ fi
30
+ uv run mike deploy "$VERSION" latest --update-aliases --push
31
+ uv run mike set-default latest --push
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: funstruct
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Fun & functional structures for Python
5
- Project-URL: Homepage, https://github.com/veyga/funstruct
5
+ Project-URL: Homepage, https://veyga.github.io/funstruct/
6
+ Project-URL: Documentation, https://veyga.github.io/funstruct/
6
7
  Project-URL: Repository, https://github.com/veyga/funstruct
7
8
  Author-email: Andrew Stefanich <andrewstefanich@gmail.com>
8
9
  License: MIT
@@ -99,6 +100,7 @@ class Monad(Applicative):
99
100
  def __rshift__ = bind # >>
100
101
 
101
102
  class MonadTransformer(Monad, Generic[_F, _A]):
103
+ def lift_f(cls, inner: _F) -> MonadTransformer: ...
102
104
  def and_then(self, other) -> MonadTransformer: ...
103
105
  ```
104
106
 
@@ -86,6 +86,7 @@ class Monad(Applicative):
86
86
  def __rshift__ = bind # >>
87
87
 
88
88
  class MonadTransformer(Monad, Generic[_F, _A]):
89
+ def lift_f(cls, inner: _F) -> MonadTransformer: ...
89
90
  def and_then(self, other) -> MonadTransformer: ...
90
91
  ```
91
92
 
@@ -17,6 +17,10 @@ from __future__ import annotations
17
17
  from abc import abstractmethod
18
18
  from collections.abc import Callable
19
19
  from dataclasses import dataclass
20
+ from typing import TYPE_CHECKING
21
+
22
+ if TYPE_CHECKING:
23
+ from funstruct.monad.result import Result
20
24
  from typing import Generic, TypeVar
21
25
 
22
26
  from funstruct.collections.cons import Cons
@@ -33,6 +37,9 @@ class Validated(Applicative):
33
37
  @abstractmethod
34
38
  def ap(self, other) -> Validated: ...
35
39
 
40
+ def __add__(self, other) -> Validated:
41
+ return self.ap(other)
42
+
36
43
  @property
37
44
  @abstractmethod
38
45
  def is_valid(self) -> bool: ...
@@ -42,6 +49,12 @@ class Validated(Applicative):
42
49
  """Eliminate the Validated — apply on_invalid or on_valid."""
43
50
  ...
44
51
 
52
+ @abstractmethod
53
+ def to_result(self) -> Result: ...
54
+
55
+ @abstractmethod
56
+ def to_result_or(self, exc_cls, combine=None) -> Result: ...
57
+
45
58
  @classmethod
46
59
  def pure(cls, value) -> Validated:
47
60
  """Lift a value into Valid."""
@@ -35,14 +35,16 @@ Examples:
35
35
  from __future__ import annotations
36
36
 
37
37
  import inspect
38
- from collections.abc import Awaitable, Callable
38
+ from abc import abstractmethod
39
+ from collections.abc import Awaitable, Callable, Coroutine
39
40
  from dataclasses import dataclass
40
41
  from functools import wraps
41
- from typing import Any, Generic, ParamSpec, TypeVar
42
+ from typing import Any, Generic, ParamSpec, TypeVar, overload
42
43
 
43
44
  from funstruct.monad.either import Either, Left, Right
44
45
 
45
46
  _A = TypeVar("_A")
47
+ _B = TypeVar("_B")
46
48
 
47
49
 
48
50
  class Result(Either[Exception, _A], Generic[_A]):
@@ -52,19 +54,32 @@ class Result(Either[Exception, _A], Generic[_A]):
52
54
  Same as Either[Exception, A] but with 1 param for cleaner annotations.
53
55
  """
54
56
 
55
- pass
57
+ @abstractmethod
58
+ def map(self, f: Callable[[_A], _B]) -> Result[_B]: ...
59
+ @abstractmethod
60
+ def bind(self, f: Callable[[_A], Result[_B]]) -> Result[_B]: ...
61
+ @abstractmethod
62
+ def alt(self, f: Callable[[Exception], Exception]) -> Result[_A]: ...
63
+ @abstractmethod
64
+ def or_else(self, f: Callable[[Exception], Result[_A]]) -> Result[_A]: ...
56
65
 
57
66
 
58
67
  @dataclass(frozen=True, eq=False)
59
68
  class Ok(Right):
60
69
  """Success case of Result."""
61
70
 
62
- def map(self, f: Callable) -> Either:
71
+ def map(self, f: Callable[[_A], _B]) -> Result[_B]:
63
72
  return Ok(f(self.value))
64
73
 
65
- def bind(self, f: Callable) -> Either:
74
+ def bind(self, f: Callable[[_A], Result[_B]]) -> Result[_B]:
66
75
  return f(self.value)
67
76
 
77
+ def alt(self, f: Callable[[Exception], Exception]) -> Result[_A]:
78
+ return self
79
+
80
+ def or_else(self, f: Callable[[Exception], Result[_A]]) -> Result[_A]:
81
+ return self
82
+
68
83
  def __repr__(self) -> str:
69
84
  return f"Ok({repr(self.value)})"
70
85
 
@@ -73,10 +88,16 @@ class Ok(Right):
73
88
  class Err(Left):
74
89
  """Error case of Result."""
75
90
 
76
- def alt(self, f: Callable) -> Either:
91
+ def map(self, f: Callable[[_A], _B]) -> Result[_B]:
92
+ return self
93
+
94
+ def bind(self, f: Callable[[_A], Result[_B]]) -> Result[_B]:
95
+ return self
96
+
97
+ def alt(self, f: Callable[[Exception], Exception]) -> Result[_A]:
77
98
  return Err(f(self.error))
78
99
 
79
- def or_else(self, f: Callable) -> Either:
100
+ def or_else(self, f: Callable[[Exception], Result[_A]]) -> Result[_A]:
80
101
  return f(self.error)
81
102
 
82
103
  def __repr__(self) -> str:
@@ -96,7 +117,7 @@ class AsyncResult(Generic[_A]):
96
117
 
97
118
  __slots__ = ("_coro",)
98
119
 
99
- def __init__(self, coro: Awaitable[Either[Exception, _A]]) -> None:
120
+ def __init__(self, coro: Awaitable[Result[_A]]) -> None:
100
121
  self._coro = coro
101
122
 
102
123
  def __del__(self):
@@ -106,10 +127,10 @@ class AsyncResult(Generic[_A]):
106
127
  def __await__(self):
107
128
  return self._awaitable().__await__()
108
129
 
109
- async def _awaitable(self) -> Either[Exception, _A]:
130
+ async def _awaitable(self) -> Result[_A]:
110
131
  return await self._coro
111
132
 
112
- def map(self, f: Callable[[_A], Any]) -> AsyncResult:
133
+ def map(self, f: Callable[[_A], _B]) -> AsyncResult[_B]:
113
134
  """Transform the success value without executing."""
114
135
 
115
136
  async def _inner():
@@ -118,11 +139,11 @@ class AsyncResult(Generic[_A]):
118
139
 
119
140
  return AsyncResult(_inner())
120
141
 
121
- def bind(self, f: Callable) -> AsyncResult:
142
+ def bind(self, f: Callable[[_A], Any]) -> AsyncResult:
122
143
  """Chain: f receives value. Short-circuits on Err.
123
144
 
124
145
  Handles all return types from f:
125
- - AsyncResult[B] → awaited, produces Either
146
+ - AsyncResult[B] → awaited, produces Result
126
147
  - Either[E, B] / Result[B] → used directly
127
148
  - Awaitable[B] → awaited, plain value wrapped in Ok
128
149
  - Plain B → wrapped in Ok
@@ -143,7 +164,7 @@ class AsyncResult(Generic[_A]):
143
164
 
144
165
  return AsyncResult(_inner())
145
166
 
146
- def alt(self, f: Callable) -> AsyncResult:
167
+ def alt(self, f: Callable[[Exception], Exception]) -> AsyncResult[_A]:
147
168
  """Transform the error without recovering."""
148
169
 
149
170
  async def _inner():
@@ -156,11 +177,11 @@ class AsyncResult(Generic[_A]):
156
177
 
157
178
  return AsyncResult(_inner())
158
179
 
159
- def or_else(self, f: Callable) -> AsyncResult:
180
+ def or_else(self, f: Callable[[Exception], Any]) -> AsyncResult:
160
181
  """Recover from error: f receives error. Short-circuits on success.
161
182
 
162
183
  Handles all return types from f:
163
- - AsyncResult[A] → awaited, produces Either
184
+ - AsyncResult[A] → awaited, produces Result
164
185
  - Either[E, A] / Result[A] → used directly
165
186
  - Awaitable[A] → awaited, plain value wrapped in Ok
166
187
  - Plain A → wrapped in Ok
@@ -181,11 +202,11 @@ class AsyncResult(Generic[_A]):
181
202
 
182
203
  return AsyncResult(_inner())
183
204
 
184
- def then(self, next_result: AsyncResult) -> AsyncResult:
205
+ def then(self, next_result: AsyncResult[_B]) -> AsyncResult[_B]:
185
206
  """Sequence: run self, discard value, run next."""
186
207
  return self.bind(lambda _: next_result)
187
208
 
188
- def ap(self, other: AsyncResult) -> AsyncResult:
209
+ def ap(self, other: AsyncResult[_B]) -> AsyncResult[tuple[_A, _B]]:
189
210
  """Applicative: run both, tuple the values."""
190
211
  return self.bind(lambda a: other.map(lambda b: (a, b)))
191
212
 
@@ -239,6 +260,14 @@ def Try(
239
260
  return wrapper
240
261
 
241
262
 
263
+ @overload
264
+ def TryAsync(
265
+ f: Callable[_P, Coroutine[Any, Any, _A]],
266
+ ) -> Callable[_P, AsyncResult[_A]]: ...
267
+ @overload
268
+ def TryAsync(
269
+ f: Callable[_P, _A],
270
+ ) -> Callable[_P, AsyncResult[_A]]: ...
242
271
  def TryAsync(
243
272
  f: Callable[_P, _A],
244
273
  ) -> Callable[_P, AsyncResult[_A]]:
@@ -54,6 +54,10 @@ nav:
54
54
  - util:
55
55
  - tailrec: util/tailrec.md
56
56
 
57
+ extra:
58
+ version:
59
+ provider: mike
60
+
57
61
  plugins:
58
62
  - search
59
63
  - mkdocstrings:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "funstruct"
3
- version = "1.0.0"
3
+ version = "1.0.2"
4
4
  description = "Fun & functional structures for Python"
5
5
  authors = [{name = "Andrew Stefanich", email = "andrewstefanich@gmail.com"}]
6
6
  license = {text = "MIT"}
@@ -10,7 +10,8 @@ keywords = ["functional", "immutable", "fp", "data structures", "cons"]
10
10
  dependencies = []
11
11
 
12
12
  [project.urls]
13
- Homepage = "https://github.com/veyga/funstruct"
13
+ Homepage = "https://veyga.github.io/funstruct/"
14
+ Documentation = "https://veyga.github.io/funstruct/"
14
15
  Repository = "https://github.com/veyga/funstruct"
15
16
 
16
17
  [build-system]
@@ -476,7 +476,7 @@ wheels = [
476
476
 
477
477
  [[package]]
478
478
  name = "funstruct"
479
- version = "1.0.0"
479
+ version = "1.0.2"
480
480
  source = { editable = "." }
481
481
 
482
482
  [package.dev-dependencies]
@@ -0,0 +1,3 @@
1
+ [[index]]
2
+ url = "https://pypi.org/simple/"
3
+ default = true
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes