mares 2026.3.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.
@@ -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,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,166 @@
1
+ # mares
2
+
3
+ **ma**rk's **res**ult class for safe data retrieval
4
+
5
+ or, as i learnt from opus 4.5, a railway-oriented two-track result pattern for
6
+ explicit success/failure handling
7
+
8
+ - [the class](#the-class)
9
+ - [an example](#an-example)
10
+
11
+ ## the class
12
+
13
+ ```python
14
+ @dataclass(frozen=True, slots=True)
15
+ class Result(Generic[T]):
16
+ """
17
+ `dataclasses.dataclass` representing a result for safe value retrieval
18
+
19
+ attributes:
20
+ `value: T`
21
+ value to return or fallback value if erroneous
22
+ `error: BaseException | None = None`
23
+ exception if any
24
+
25
+ methods:
26
+ `def __bool__(self) -> bool: ...`
27
+ method for boolean comparison for exception safety
28
+ `def get(self) -> T: ...`
29
+ method that raises or returns an error if the Result is erroneous
30
+ `def map(self, func: Callable[[T], U]) -> Result[U]: ...`
31
+ method that maps the value when not erroneous
32
+ `def bind(self, func: Callable[[T], Result[U]]) -> Result[U]: ...`
33
+ method that binds to another Result-returning function
34
+ `def cry(self, string: bool = False) -> str: ...`
35
+ method that returns the result value or raises an error
36
+ """
37
+
38
+ value: T
39
+ error: BaseException | None = None
40
+
41
+ def __bool__(self) -> bool:
42
+ """
43
+ method for boolean comparison for easier exception handling
44
+
45
+ returns: `bool`
46
+ that returns True if `self.error` is not None
47
+ """
48
+ return self.error is None
49
+
50
+ def cry(self, string: bool = False) -> str: # noqa: FBT001, FBT002
51
+ """
52
+ method that raises or returns an error if the Result is erroneous
53
+
54
+ arguments:
55
+ `string: bool = False`
56
+ if `self.error` is an Exception, returns it as a string
57
+ error message
58
+
59
+ returns: `str`
60
+ returns `self.error` as a string if `string` is True,
61
+ or returns an empty string if `self.error` is None
62
+ """
63
+
64
+ if isinstance(self.error, BaseException):
65
+ if string:
66
+ message = f"{self.error}"
67
+ name = self.error.__class__.__name__
68
+ return f"{message} ({name})" if (message != "") else name
69
+
70
+ raise self.error
71
+
72
+ return ""
73
+
74
+ def get(self) -> T:
75
+ """
76
+ method that returns the result value or raises an error
77
+
78
+ returns: `T`
79
+ returns `self.value` if `self.error` is None
80
+
81
+ raises: `BaseException`
82
+ if `self.error` is not None
83
+ """
84
+ if self.error is not None:
85
+ raise self.error
86
+ return self.value
87
+
88
+ def map(self, func: Callable[[T], U]) -> "Result[U]":
89
+ """
90
+ method that maps the value when not erroneous
91
+
92
+ arguments:
93
+ `func: Callable[[T], U]`
94
+ function to transform the value
95
+
96
+ returns: `Result[U]`
97
+ returns a new Result with the transformed value, or the same error
98
+ """
99
+ if self.error is not None:
100
+ return Result(cast(U, self.value), error=self.error)
101
+ return Result(func(self.value))
102
+
103
+ def bind(self, func: Callable[[T], "Result[U]"]) -> "Result[U]":
104
+ """
105
+ method that binds to another Result-returning function
106
+
107
+ arguments:
108
+ `func: Callable[[T], Result[U]]`
109
+ function to transform the value into a Result
110
+
111
+ returns: `Result[U]`
112
+ returns the bound Result, or the same error
113
+ """
114
+ if self.error is not None:
115
+ return Result(cast(U, self.value), error=self.error)
116
+ return func(self.value)
117
+
118
+ @staticmethod
119
+ def wrap(default: R) -> Callable[[Callable[P, R]], Callable[P, "Result[R]"]]:
120
+ """decorator that wraps a non-Result-returning function to return a Result"""
121
+
122
+ def result_decorator(func: Callable[P, R]) -> Callable[P, Result[R]]:
123
+ @wraps(func)
124
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[R]:
125
+ try:
126
+ return Result(func(*args, **kwargs))
127
+ except Exception as exc:
128
+ return Result(default, error=exc)
129
+
130
+ return wrapper
131
+
132
+ return result_decorator
133
+ ```
134
+
135
+ ## an example
136
+
137
+ ```python
138
+ def fetch_json(url: str) -> Result[dict[str, Any]]:
139
+ try:
140
+ with urllib.request.urlopen(url, timeout=10) as response:
141
+ data = response.read().decode("utf-8")
142
+ return Result(json.loads(data))
143
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
144
+ return Result({}, error=exc)
145
+
146
+ def require_field(data: dict[str, Any], field: str) -> Result[dict[str, Any]]:
147
+ if field not in data:
148
+ return Result({}, error=KeyError(f"Missing field: {field}"))
149
+ return Result(data)
150
+
151
+ return (
152
+ fetch_json(f"https://jsonplaceholder.typicode.com/users/{randint(1, 10)}")
153
+ .bind(lambda payload: require_field(payload, "name"))
154
+ .map(lambda payload: str(payload["name"]))
155
+ )
156
+ ```
157
+
158
+ **bonus!** this is available as a library, and as a quick insertion tool.
159
+
160
+ ```
161
+ pip install mares
162
+
163
+ uv install mares
164
+
165
+ uvx mares inject path/to/file.py
166
+ ```
@@ -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/>
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "mares"
3
+ version = "2026.3.2"
4
+ description = "mark's result class for safe value retrieval"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Mark Joshwel", email = "mark@joshwel.co" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = []
11
+ classifiers = [
12
+ "Development Status :: 5 - Production/Stable",
13
+ "Intended Audience :: Developers",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Typing :: Typed",
24
+ ]
25
+ license = "Unlicense OR 0BSD"
26
+ license-files = ["LICENCING", "LICENSE-0BSD", "UNLICENSE"]
27
+
28
+ [project.scripts]
29
+ mares = "mares.mares:cli"
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.9.28,<0.10.0"]
33
+ build-backend = "uv_build"
34
+
35
+ [dependency-groups]
36
+ dev = [
37
+ "basedpyright>=1.37.3",
38
+ "mypy>=1.19.1",
39
+ "ruff>=0.14.14",
40
+ ]
@@ -0,0 +1,9 @@
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 .mares import Result, __VERSION__
8
+
9
+ __all__ = ["Result", "__VERSION__"]
@@ -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()
File without changes