mares 2026.3.2__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.
- mares/__init__.py +9 -0
- mares/mares.py +343 -0
- mares/py.typed +0 -0
- mares-2026.3.2.dist-info/METADATA +191 -0
- mares-2026.3.2.dist-info/RECORD +10 -0
- mares-2026.3.2.dist-info/WHEEL +4 -0
- mares-2026.3.2.dist-info/entry_points.txt +3 -0
- mares-2026.3.2.dist-info/licenses/LICENCING +25 -0
- mares-2026.3.2.dist-info/licenses/LICENSE-0BSD +12 -0
- mares-2026.3.2.dist-info/licenses/UNLICENSE +24 -0
mares/__init__.py
ADDED
mares/mares.py
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mares: mark's result class for safe value retrieval
|
|
3
|
+
with all my heart, 2023-2026, mark joshwel <mark@joshwel.co>
|
|
4
|
+
SPDX-License-Identifier: Unlicense OR 0BSD
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from sys import argv, stderr, stdin, stdout
|
|
11
|
+
from typing import Callable, Generic, NoReturn, ParamSpec, TypeVar, cast
|
|
12
|
+
|
|
13
|
+
__VERSION__ = "2026.2.3"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
P = ParamSpec("P")
|
|
18
|
+
R = TypeVar("R")
|
|
19
|
+
U = TypeVar("U")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True, slots=True)
|
|
23
|
+
class Result(Generic[T]):
|
|
24
|
+
"""
|
|
25
|
+
`dataclasses.dataclass` representing a result for safe value retrieval
|
|
26
|
+
|
|
27
|
+
attributes:
|
|
28
|
+
`value: T`
|
|
29
|
+
value to return or fallback value if erroneous
|
|
30
|
+
`error: BaseException | None = None`
|
|
31
|
+
exception if any
|
|
32
|
+
|
|
33
|
+
methods:
|
|
34
|
+
`def __bool__(self) -> bool: ...`
|
|
35
|
+
method for boolean comparison for exception safety
|
|
36
|
+
`def get(self) -> T: ...`
|
|
37
|
+
method that raises or returns an error if the Result is erroneous
|
|
38
|
+
`def map(self, func: Callable[[T], U]) -> Result[U]: ...`
|
|
39
|
+
method that maps the value when not erroneous
|
|
40
|
+
`def bind(self, func: Callable[[T], Result[U]]) -> Result[U]: ...`
|
|
41
|
+
method that binds to another Result-returning function
|
|
42
|
+
`def cry(self, string: bool = False) -> str: ...`
|
|
43
|
+
method that returns the result value or raises an error
|
|
44
|
+
|
|
45
|
+
usage:
|
|
46
|
+
```python
|
|
47
|
+
# do something
|
|
48
|
+
def wrapped_read(path: str) -> Result[str]:
|
|
49
|
+
try:
|
|
50
|
+
with open(path, encoding="utf-8") as file:
|
|
51
|
+
contents = file.read()
|
|
52
|
+
except Exception as exc:
|
|
53
|
+
# must pass a default value
|
|
54
|
+
return Result[str]("", error=exc)
|
|
55
|
+
else:
|
|
56
|
+
return Result[str](contents)
|
|
57
|
+
|
|
58
|
+
# call function and handle result
|
|
59
|
+
# and check if the result is erroneous
|
|
60
|
+
result = wrapped_read("some_file.txt")
|
|
61
|
+
|
|
62
|
+
if not result:
|
|
63
|
+
# .cry() raises the exception
|
|
64
|
+
# (or returns it as a string error message using string=True)
|
|
65
|
+
print(f"error: {result.cry()}")
|
|
66
|
+
exit()
|
|
67
|
+
else:
|
|
68
|
+
# .get() raises exception or returns value,
|
|
69
|
+
# but since we checked for errors this is safe
|
|
70
|
+
print(result.get())
|
|
71
|
+
|
|
72
|
+
# railway-oriented example
|
|
73
|
+
def parse_int(text: str) -> Result[int]:
|
|
74
|
+
try:
|
|
75
|
+
return Result[int](int(text.strip()))
|
|
76
|
+
except ValueError as exc:
|
|
77
|
+
return Result[int](0, error=exc)
|
|
78
|
+
|
|
79
|
+
chained = (
|
|
80
|
+
wrapped_read("some_file.txt")
|
|
81
|
+
.bind(parse_int)
|
|
82
|
+
.map(lambda value: value * 2)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if not chained:
|
|
86
|
+
print(f"error: {chained.cry()}")
|
|
87
|
+
else:
|
|
88
|
+
print(chained.get())
|
|
89
|
+
```
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
value: T
|
|
93
|
+
error: BaseException | None = None
|
|
94
|
+
|
|
95
|
+
def __bool__(self) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
method for boolean comparison for easier exception handling
|
|
98
|
+
|
|
99
|
+
returns: `bool`
|
|
100
|
+
that returns True if `self.error` is not None
|
|
101
|
+
"""
|
|
102
|
+
return self.error is None
|
|
103
|
+
|
|
104
|
+
def cry(self, string: bool = False) -> str: # noqa: FBT001, FBT002
|
|
105
|
+
"""
|
|
106
|
+
method that raises or returns an error if the Result is erroneous
|
|
107
|
+
|
|
108
|
+
arguments:
|
|
109
|
+
`string: bool = False`
|
|
110
|
+
if `self.error` is an Exception, returns it as a string
|
|
111
|
+
error message
|
|
112
|
+
|
|
113
|
+
returns: `str`
|
|
114
|
+
returns `self.error` as a string if `string` is True,
|
|
115
|
+
or returns an empty string if `self.error` is None
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
if isinstance(self.error, BaseException):
|
|
119
|
+
if string:
|
|
120
|
+
message = f"{self.error}"
|
|
121
|
+
name = self.error.__class__.__name__
|
|
122
|
+
return f"{message} ({name})" if (message != "") else name
|
|
123
|
+
|
|
124
|
+
raise self.error
|
|
125
|
+
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
def get(self) -> T:
|
|
129
|
+
"""
|
|
130
|
+
method that returns the result value or raises an error
|
|
131
|
+
|
|
132
|
+
returns: `T`
|
|
133
|
+
returns `self.value` if `self.error` is None
|
|
134
|
+
|
|
135
|
+
raises: `BaseException`
|
|
136
|
+
if `self.error` is not None
|
|
137
|
+
"""
|
|
138
|
+
if self.error is not None:
|
|
139
|
+
raise self.error
|
|
140
|
+
return self.value
|
|
141
|
+
|
|
142
|
+
def map(self, func: Callable[[T], U]) -> "Result[U]":
|
|
143
|
+
"""
|
|
144
|
+
method that maps the value when not erroneous
|
|
145
|
+
|
|
146
|
+
arguments:
|
|
147
|
+
`func: Callable[[T], U]`
|
|
148
|
+
function to transform the value
|
|
149
|
+
|
|
150
|
+
returns: `Result[U]`
|
|
151
|
+
returns a new Result with the transformed value, or the same error
|
|
152
|
+
"""
|
|
153
|
+
if self.error is not None:
|
|
154
|
+
return Result(cast(U, self.value), error=self.error)
|
|
155
|
+
return Result(func(self.value))
|
|
156
|
+
|
|
157
|
+
def bind(self, func: Callable[[T], "Result[U]"]) -> "Result[U]":
|
|
158
|
+
"""
|
|
159
|
+
method that binds to another Result-returning function
|
|
160
|
+
|
|
161
|
+
arguments:
|
|
162
|
+
`func: Callable[[T], Result[U]]`
|
|
163
|
+
function to transform the value into a Result
|
|
164
|
+
|
|
165
|
+
returns: `Result[U]`
|
|
166
|
+
returns the bound Result, or the same error
|
|
167
|
+
"""
|
|
168
|
+
if self.error is not None:
|
|
169
|
+
return Result(cast(U, self.value), error=self.error)
|
|
170
|
+
return func(self.value)
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def wrap(default: R) -> Callable[[Callable[P, R]], Callable[P, "Result[R]"]]:
|
|
174
|
+
"""decorator that wraps a non-Result-returning function to return a Result"""
|
|
175
|
+
|
|
176
|
+
def result_decorator(func: Callable[P, R]) -> Callable[P, Result[R]]:
|
|
177
|
+
@wraps(func)
|
|
178
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[R]:
|
|
179
|
+
try:
|
|
180
|
+
return Result(func(*args, **kwargs))
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
return Result(default, error=exc)
|
|
183
|
+
|
|
184
|
+
return wrapper
|
|
185
|
+
|
|
186
|
+
return result_decorator
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _result_snippet() -> str:
|
|
190
|
+
lines = Path(__file__).read_text(encoding="utf-8").splitlines()
|
|
191
|
+
snippet_lines = lines[15:186]
|
|
192
|
+
return "\n".join(snippet_lines)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _add_imports(lines: list[str]) -> list[str]:
|
|
196
|
+
required = [
|
|
197
|
+
"from dataclasses import dataclass",
|
|
198
|
+
"from functools import wraps",
|
|
199
|
+
"from typing import Callable, Generic, ParamSpec, TypeVar, cast",
|
|
200
|
+
]
|
|
201
|
+
missing = [line for line in required if line not in lines]
|
|
202
|
+
if not missing:
|
|
203
|
+
return lines
|
|
204
|
+
insert_at = 0
|
|
205
|
+
if lines and lines[0].startswith("#!"):
|
|
206
|
+
insert_at = 1
|
|
207
|
+
while insert_at < len(lines) and lines[insert_at].startswith(
|
|
208
|
+
"from __future__ import"
|
|
209
|
+
):
|
|
210
|
+
insert_at += 1
|
|
211
|
+
updated = lines[:insert_at] + missing + [""] + lines[insert_at:]
|
|
212
|
+
return updated
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _replace_marker(text: str, snippet: str) -> str:
|
|
216
|
+
lines: list[str] = text.splitlines()
|
|
217
|
+
marker_index = next(
|
|
218
|
+
(
|
|
219
|
+
current_index
|
|
220
|
+
for current_index, line in enumerate(lines)
|
|
221
|
+
if line == "# mares"
|
|
222
|
+
),
|
|
223
|
+
None,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if marker_index is None:
|
|
227
|
+
_die("mares: error: could not find a '# mares' marker to replace code with")
|
|
228
|
+
|
|
229
|
+
lines[marker_index : marker_index + 1] = snippet.splitlines()
|
|
230
|
+
|
|
231
|
+
output = "\n".join(lines)
|
|
232
|
+
if text.endswith("\n"):
|
|
233
|
+
output += "\n"
|
|
234
|
+
return output
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
CLI_HELP = """mares: cli insertion tool for mark's result class for safe value retrieval
|
|
238
|
+
|
|
239
|
+
usage:
|
|
240
|
+
mares insert [<path>] [--dont-import] [--read-from-stdin] [--write-to-stdout]
|
|
241
|
+
mares insert --read-from-stdin <path> [--dont-import]
|
|
242
|
+
mares insert --write-to-stdout [--dont-import]
|
|
243
|
+
mares --help
|
|
244
|
+
mares --version
|
|
245
|
+
|
|
246
|
+
note: replaces a line exactly matching "# mares"
|
|
247
|
+
note: --write-to-stdout with no path prints the Result code block
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _print_help() -> None:
|
|
252
|
+
_ = stdout.write(CLI_HELP)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _die(message: str | None = None) -> NoReturn:
|
|
256
|
+
if message:
|
|
257
|
+
print(message, file=stderr)
|
|
258
|
+
raise SystemExit(1)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def cli() -> None:
|
|
262
|
+
args: list[str] = argv[1:]
|
|
263
|
+
if not args:
|
|
264
|
+
print("mares: error: missing command\n", file=stderr)
|
|
265
|
+
_print_help()
|
|
266
|
+
_die()
|
|
267
|
+
|
|
268
|
+
if "--version" in args or "-V" in args:
|
|
269
|
+
_ = stdout.write(f"{__VERSION__}\n")
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
if args[0] in {"--help", "-h"}:
|
|
273
|
+
_print_help()
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
command = args[0]
|
|
277
|
+
if command != "insert":
|
|
278
|
+
_die(f"mares: error: unknown command {command}")
|
|
279
|
+
|
|
280
|
+
if "--help" in args[1:] or "-h" in args[1:]:
|
|
281
|
+
_print_help()
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
flags: set[str] = set()
|
|
285
|
+
positionals: list[str] = []
|
|
286
|
+
for item in args[1:]:
|
|
287
|
+
if item in {"--dont-import", "--read-from-stdin", "--write-to-stdout"}:
|
|
288
|
+
flags.add(item)
|
|
289
|
+
elif item.startswith("--"):
|
|
290
|
+
_die(f"mares: error: unknown option {item}")
|
|
291
|
+
else:
|
|
292
|
+
positionals.append(item)
|
|
293
|
+
|
|
294
|
+
if len(positionals) > 1:
|
|
295
|
+
_die("mares: error: too many arguments")
|
|
296
|
+
|
|
297
|
+
path: Path | None = Path(positionals[0]) if positionals else None
|
|
298
|
+
read_from_stdin: bool = "--read-from-stdin" in flags
|
|
299
|
+
write_to_stdout: bool = "--write-to-stdout" in flags
|
|
300
|
+
dont_import: bool = "--dont-import" in flags
|
|
301
|
+
|
|
302
|
+
if path is None and not read_from_stdin and not write_to_stdout:
|
|
303
|
+
_die("mares: error: missing path to insert into")
|
|
304
|
+
|
|
305
|
+
snippet: str = _result_snippet()
|
|
306
|
+
if path is None and write_to_stdout and not read_from_stdin:
|
|
307
|
+
snippet_lines = snippet.splitlines()
|
|
308
|
+
if not dont_import:
|
|
309
|
+
snippet_lines = _add_imports(snippet_lines)
|
|
310
|
+
output_text = "\n".join(snippet_lines)
|
|
311
|
+
if snippet.endswith("\n"):
|
|
312
|
+
output_text += "\n"
|
|
313
|
+
_ = stdout.write(output_text)
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
if read_from_stdin:
|
|
317
|
+
original: str = stdin.read()
|
|
318
|
+
else:
|
|
319
|
+
if path is None:
|
|
320
|
+
_die("mares: error: missing path to insert into")
|
|
321
|
+
original = path.read_text(encoding="utf-8")
|
|
322
|
+
|
|
323
|
+
replaced: str = _replace_marker(original, snippet)
|
|
324
|
+
lines: list[str] = replaced.splitlines()
|
|
325
|
+
|
|
326
|
+
if not dont_import:
|
|
327
|
+
lines = _add_imports(lines)
|
|
328
|
+
|
|
329
|
+
output: str = "\n".join(lines)
|
|
330
|
+
if replaced.endswith("\n"):
|
|
331
|
+
output += "\n"
|
|
332
|
+
|
|
333
|
+
if write_to_stdout:
|
|
334
|
+
_ = stdout.write(output)
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
if path is None:
|
|
338
|
+
_die("mares: error: missing path to write output to")
|
|
339
|
+
_ = path.write_text(output, encoding="utf-8")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
cli()
|
mares/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mares
|
|
3
|
+
Version: 2026.3.2
|
|
4
|
+
Summary: mark's result class for safe value retrieval
|
|
5
|
+
Author: Mark Joshwel
|
|
6
|
+
Author-email: Mark Joshwel <mark@joshwel.co>
|
|
7
|
+
License-Expression: Unlicense OR 0BSD
|
|
8
|
+
License-File: LICENCING
|
|
9
|
+
License-File: LICENSE-0BSD
|
|
10
|
+
License-File: UNLICENSE
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.13
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# mares
|
|
27
|
+
|
|
28
|
+
**ma**rk's **res**ult class for safe data retrieval
|
|
29
|
+
|
|
30
|
+
or, as i learnt from opus 4.5, a railway-oriented two-track result pattern for
|
|
31
|
+
explicit success/failure handling
|
|
32
|
+
|
|
33
|
+
- [the class](#the-class)
|
|
34
|
+
- [an example](#an-example)
|
|
35
|
+
|
|
36
|
+
## the class
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class Result(Generic[T]):
|
|
41
|
+
"""
|
|
42
|
+
`dataclasses.dataclass` representing a result for safe value retrieval
|
|
43
|
+
|
|
44
|
+
attributes:
|
|
45
|
+
`value: T`
|
|
46
|
+
value to return or fallback value if erroneous
|
|
47
|
+
`error: BaseException | None = None`
|
|
48
|
+
exception if any
|
|
49
|
+
|
|
50
|
+
methods:
|
|
51
|
+
`def __bool__(self) -> bool: ...`
|
|
52
|
+
method for boolean comparison for exception safety
|
|
53
|
+
`def get(self) -> T: ...`
|
|
54
|
+
method that raises or returns an error if the Result is erroneous
|
|
55
|
+
`def map(self, func: Callable[[T], U]) -> Result[U]: ...`
|
|
56
|
+
method that maps the value when not erroneous
|
|
57
|
+
`def bind(self, func: Callable[[T], Result[U]]) -> Result[U]: ...`
|
|
58
|
+
method that binds to another Result-returning function
|
|
59
|
+
`def cry(self, string: bool = False) -> str: ...`
|
|
60
|
+
method that returns the result value or raises an error
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
value: T
|
|
64
|
+
error: BaseException | None = None
|
|
65
|
+
|
|
66
|
+
def __bool__(self) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
method for boolean comparison for easier exception handling
|
|
69
|
+
|
|
70
|
+
returns: `bool`
|
|
71
|
+
that returns True if `self.error` is not None
|
|
72
|
+
"""
|
|
73
|
+
return self.error is None
|
|
74
|
+
|
|
75
|
+
def cry(self, string: bool = False) -> str: # noqa: FBT001, FBT002
|
|
76
|
+
"""
|
|
77
|
+
method that raises or returns an error if the Result is erroneous
|
|
78
|
+
|
|
79
|
+
arguments:
|
|
80
|
+
`string: bool = False`
|
|
81
|
+
if `self.error` is an Exception, returns it as a string
|
|
82
|
+
error message
|
|
83
|
+
|
|
84
|
+
returns: `str`
|
|
85
|
+
returns `self.error` as a string if `string` is True,
|
|
86
|
+
or returns an empty string if `self.error` is None
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
if isinstance(self.error, BaseException):
|
|
90
|
+
if string:
|
|
91
|
+
message = f"{self.error}"
|
|
92
|
+
name = self.error.__class__.__name__
|
|
93
|
+
return f"{message} ({name})" if (message != "") else name
|
|
94
|
+
|
|
95
|
+
raise self.error
|
|
96
|
+
|
|
97
|
+
return ""
|
|
98
|
+
|
|
99
|
+
def get(self) -> T:
|
|
100
|
+
"""
|
|
101
|
+
method that returns the result value or raises an error
|
|
102
|
+
|
|
103
|
+
returns: `T`
|
|
104
|
+
returns `self.value` if `self.error` is None
|
|
105
|
+
|
|
106
|
+
raises: `BaseException`
|
|
107
|
+
if `self.error` is not None
|
|
108
|
+
"""
|
|
109
|
+
if self.error is not None:
|
|
110
|
+
raise self.error
|
|
111
|
+
return self.value
|
|
112
|
+
|
|
113
|
+
def map(self, func: Callable[[T], U]) -> "Result[U]":
|
|
114
|
+
"""
|
|
115
|
+
method that maps the value when not erroneous
|
|
116
|
+
|
|
117
|
+
arguments:
|
|
118
|
+
`func: Callable[[T], U]`
|
|
119
|
+
function to transform the value
|
|
120
|
+
|
|
121
|
+
returns: `Result[U]`
|
|
122
|
+
returns a new Result with the transformed value, or the same error
|
|
123
|
+
"""
|
|
124
|
+
if self.error is not None:
|
|
125
|
+
return Result(cast(U, self.value), error=self.error)
|
|
126
|
+
return Result(func(self.value))
|
|
127
|
+
|
|
128
|
+
def bind(self, func: Callable[[T], "Result[U]"]) -> "Result[U]":
|
|
129
|
+
"""
|
|
130
|
+
method that binds to another Result-returning function
|
|
131
|
+
|
|
132
|
+
arguments:
|
|
133
|
+
`func: Callable[[T], Result[U]]`
|
|
134
|
+
function to transform the value into a Result
|
|
135
|
+
|
|
136
|
+
returns: `Result[U]`
|
|
137
|
+
returns the bound Result, or the same error
|
|
138
|
+
"""
|
|
139
|
+
if self.error is not None:
|
|
140
|
+
return Result(cast(U, self.value), error=self.error)
|
|
141
|
+
return func(self.value)
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def wrap(default: R) -> Callable[[Callable[P, R]], Callable[P, "Result[R]"]]:
|
|
145
|
+
"""decorator that wraps a non-Result-returning function to return a Result"""
|
|
146
|
+
|
|
147
|
+
def result_decorator(func: Callable[P, R]) -> Callable[P, Result[R]]:
|
|
148
|
+
@wraps(func)
|
|
149
|
+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[R]:
|
|
150
|
+
try:
|
|
151
|
+
return Result(func(*args, **kwargs))
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
return Result(default, error=exc)
|
|
154
|
+
|
|
155
|
+
return wrapper
|
|
156
|
+
|
|
157
|
+
return result_decorator
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## an example
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
def fetch_json(url: str) -> Result[dict[str, Any]]:
|
|
164
|
+
try:
|
|
165
|
+
with urllib.request.urlopen(url, timeout=10) as response:
|
|
166
|
+
data = response.read().decode("utf-8")
|
|
167
|
+
return Result(json.loads(data))
|
|
168
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
169
|
+
return Result({}, error=exc)
|
|
170
|
+
|
|
171
|
+
def require_field(data: dict[str, Any], field: str) -> Result[dict[str, Any]]:
|
|
172
|
+
if field not in data:
|
|
173
|
+
return Result({}, error=KeyError(f"Missing field: {field}"))
|
|
174
|
+
return Result(data)
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
fetch_json(f"https://jsonplaceholder.typicode.com/users/{randint(1, 10)}")
|
|
178
|
+
.bind(lambda payload: require_field(payload, "name"))
|
|
179
|
+
.map(lambda payload: str(payload["name"]))
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**bonus!** this is available as a library, and as a quick insertion tool.
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
pip install mares
|
|
187
|
+
|
|
188
|
+
uv install mares
|
|
189
|
+
|
|
190
|
+
uvx mares inject path/to/file.py
|
|
191
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
mares/__init__.py,sha256=8Ec-AjFjo6w0QelCeHzmZyRcqzFatKJ9A4TSuNNVV-g,254
|
|
2
|
+
mares/mares.py,sha256=5jMxkn9M-AFtULdncVng4n15f5AH8jKr4s_73t074m0,10299
|
|
3
|
+
mares/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
mares-2026.3.2.dist-info/licenses/LICENCING,sha256=cvXCbqLSG6gBjkXvVDGU2GpT-1ZJLp5lrRoCNaaAm1E,1072
|
|
5
|
+
mares-2026.3.2.dist-info/licenses/LICENSE-0BSD,sha256=rKJ-BtUtVzvJJxPGJ1b6WkMxa7GrxuOFo87DIk_IgMk,677
|
|
6
|
+
mares-2026.3.2.dist-info/licenses/UNLICENSE,sha256=8Bl77UGlO95Tuu1FjTzqAPr-UU_A11XBQdPct1-E3qE,1236
|
|
7
|
+
mares-2026.3.2.dist-info/WHEEL,sha256=fAguSjoiATBe7TNBkJwOjyL1Tt4wwiaQGtNtjRPNMQA,80
|
|
8
|
+
mares-2026.3.2.dist-info/entry_points.txt,sha256=0OM-oTIuJ2o1oBTqJskJye_O2gTanxJb3_x-1zA9F8s,43
|
|
9
|
+
mares-2026.3.2.dist-info/METADATA,sha256=9QXfa1kNlmVZBBOqs5r3cycFKRNPfER8iet6yfm7wvE,6231
|
|
10
|
+
mares-2026.3.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
This software is unencumbered and free-as-in-freedom.
|
|
2
|
+
|
|
3
|
+
This project is dual-licensed under The Unlicense or the BSD Zero Clause
|
|
4
|
+
License. (SPDX: `Unlicense OR 0BSD`)
|
|
5
|
+
|
|
6
|
+
The spirit and intent of the project is to treat the work as public domain via
|
|
7
|
+
The Unlicense, but providing the BSD Zero Clause License where public domain
|
|
8
|
+
dedication is not possible due to policies bound to a contributor or their
|
|
9
|
+
employer. However, this is not a legal condition.
|
|
10
|
+
|
|
11
|
+
Either way, you are free to use the software as you wish, without any
|
|
12
|
+
restrictions or obligations, subject only to the warranty disclaimers in the
|
|
13
|
+
licence texts.
|
|
14
|
+
|
|
15
|
+
> Does dual licencing not invalidate the ethos behind The Unlicense?
|
|
16
|
+
|
|
17
|
+
Well yeah, but like, even if ten percent of code is 0BSD-licenced, the rest
|
|
18
|
+
is still unlicenced. And that's... good enough, and better than the
|
|
19
|
+
alternative of a fully 0BSD-licenced project. _Everyone should be able to
|
|
20
|
+
contribute._
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
See the [CONTRIBUTING](./CONTRIBUTING), [UNLICENSE](./UNLICENSE), and
|
|
25
|
+
[LICENSE-0BSD](./LICENSE-0BSD) files for more information.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (C) 2025 by Mark Joshwel <mark@joshwel.co>
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for
|
|
4
|
+
any purpose with or without fee is hereby granted.
|
|
5
|
+
|
|
6
|
+
THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
|
|
7
|
+
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
|
|
8
|
+
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
|
|
9
|
+
FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
|
|
10
|
+
DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
|
|
11
|
+
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
|
12
|
+
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org/>
|