paramspecli 0.2.1__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.
paramspecli/flags.py ADDED
@@ -0,0 +1,226 @@
1
+ from argparse import BooleanOptionalAction
2
+ from typing import Any, Literal, overload
3
+
4
+ from .fake import Option, RepeatedOption
5
+
6
+
7
+ ##
8
+ # common use - set: True, unset: False
9
+ @overload
10
+ def flag( # type: ignore[overload-overlap]
11
+ *names: str,
12
+ value: bool = True,
13
+ help: str | Literal[False] | None = None,
14
+ show_default: bool | str | None = None,
15
+ ) -> Option[bool, bool]: ...
16
+
17
+
18
+ ## value: T
19
+ # even less common use - set: T, unset: None
20
+ @overload
21
+ def flag[T](
22
+ *names: str,
23
+ value: T,
24
+ help: str | Literal[False] | None = None,
25
+ show_default: bool | str | None = None,
26
+ ) -> Option[T, None]: ...
27
+
28
+
29
+ ## value: T, default: D
30
+ # even even less common use - set: T, unset: D
31
+ @overload
32
+ def flag[T, D](
33
+ *names: str,
34
+ value: T,
35
+ default: D,
36
+ help: str | Literal[False] | None = None,
37
+ show_default: bool | str | None = None,
38
+ ) -> Option[T, D]: ...
39
+
40
+
41
+ ## default: D
42
+ # for completeness. shouldn't be really used
43
+ @overload
44
+ def flag[D](
45
+ *names: str,
46
+ default: D,
47
+ value: bool = True,
48
+ help: str | Literal[False] | None = None,
49
+ show_default: bool | str | None = None,
50
+ ) -> Option[bool, D]: ...
51
+
52
+
53
+ # NOTE: using singleton ellipsis here as the sentinel
54
+ def flag(
55
+ *names: str,
56
+ value: Any = True,
57
+ default: Any = ...,
58
+ help: str | bool | None = None,
59
+ show_default: bool | str | None = None,
60
+ ) -> Option[Any, Any]:
61
+ """flag
62
+
63
+ In basic configuration it's boolean:
64
+ ```
65
+ flag() -> True if --foo else False
66
+ ```
67
+ Pass `value=False` to invert:
68
+ ```
69
+ flag(value=False) -> False if --foo else True
70
+ ```
71
+
72
+ Pass non-bool `value` to disable the boolean mode:
73
+ ```
74
+ flag(value=123) -> 123 if --foo else None
75
+ ```
76
+ Pass `default` to change default:
77
+ ```
78
+ flag(value=123, default=456) -> 123 if --foo else 456
79
+ ```
80
+
81
+
82
+ """
83
+ if default is ...:
84
+ soft_show_default = False
85
+ if value is True:
86
+ default = False
87
+ elif value is False:
88
+ default = True
89
+ else:
90
+ default = None
91
+ else:
92
+ soft_show_default = default is not None
93
+
94
+ return Option(
95
+ names,
96
+ help=help,
97
+ action="store_const",
98
+ const=value,
99
+ default=default,
100
+ #
101
+ hard_show_default=show_default,
102
+ soft_show_default=soft_show_default,
103
+ )
104
+
105
+
106
+ ## -> bool
107
+ @overload
108
+ def switch(
109
+ *names: str,
110
+ default: bool = False,
111
+ help: str | Literal[False] | None = None,
112
+ show_default: bool | str | None = None,
113
+ ) -> Option[bool, bool]: ...
114
+
115
+
116
+ ## default: D
117
+ @overload
118
+ def switch[D](
119
+ *names: str,
120
+ default: D,
121
+ help: str | Literal[False] | None = None,
122
+ show_default: bool | str | None = None,
123
+ ) -> Option[bool, D]: ...
124
+
125
+
126
+ def switch(
127
+ *names: str,
128
+ default: Any = False,
129
+ help: str | bool | None = None,
130
+ show_default: bool | str | None = None,
131
+ ) -> Option[bool, Any]:
132
+ """On/Off switch with complimentary flags: `--foo/--no-foo`. Default is `False`"""
133
+ longs = [name for name in names if name.startswith("--")]
134
+ if not longs:
135
+ raise ValueError("at least one name starting with '--' is required")
136
+
137
+ soft_show_default: bool | str
138
+
139
+ if default is True or default is False:
140
+ soft_show_default = longs[0] if default else f"--no-{longs[0][2:]}"
141
+ else:
142
+ soft_show_default = True
143
+
144
+ return Option(
145
+ names,
146
+ help=help,
147
+ action=BooleanOptionalAction,
148
+ default=default,
149
+ #
150
+ hard_show_default=show_default,
151
+ soft_show_default=soft_show_default,
152
+ )
153
+
154
+
155
+ ## -> int
156
+ @overload
157
+ def count(
158
+ *names: str,
159
+ default: int = 0,
160
+ help: str | Literal[False] | None = None,
161
+ show_default: bool | str | None = None,
162
+ ) -> Option[int, int]: ...
163
+
164
+
165
+ ## default: None
166
+ @overload
167
+ def count(
168
+ *names: str,
169
+ default: None,
170
+ help: str | Literal[False] | None = None,
171
+ show_default: bool | str | None = None,
172
+ ) -> Option[int, None]: ...
173
+
174
+
175
+ def count(
176
+ *names: str,
177
+ default: int | None = 0,
178
+ help: str | bool | None = None,
179
+ show_default: bool | str | None = None,
180
+ ) -> Option[int, Any]:
181
+ """Counter: `-vvv`. Default is `0`"""
182
+ return Option(
183
+ names,
184
+ help=help,
185
+ action="count",
186
+ default=default,
187
+ #
188
+ hard_show_default=show_default,
189
+ soft_show_default=False,
190
+ )
191
+
192
+
193
+ ##
194
+ @overload
195
+ def repeated_flag(
196
+ *names: str,
197
+ value: bool = True,
198
+ help: str | Literal[False] | None = None,
199
+ ) -> RepeatedOption[bool]: ...
200
+
201
+
202
+ # value: T
203
+ @overload
204
+ def repeated_flag[T](
205
+ *names: str,
206
+ value: T,
207
+ help: str | Literal[False] | None = None,
208
+ ) -> RepeatedOption[T]: ...
209
+
210
+
211
+ def repeated_flag(
212
+ *names: str,
213
+ value: Any = True,
214
+ help: str | bool | None = None,
215
+ ) -> RepeatedOption[Any]:
216
+ """Flag which could be present multiple times on a command line.
217
+ Each appearance adds `value` to the list.
218
+ """
219
+ return RepeatedOption(
220
+ names,
221
+ help=help,
222
+ action="append_const",
223
+ default=[],
224
+ const=value,
225
+ soft_show_default=False,
226
+ )
paramspecli/md.py ADDED
@@ -0,0 +1,75 @@
1
+ import re
2
+ from typing import Iterable
3
+
4
+ # NOTE: dots are not escaped
5
+ _MD_ESCAPE = re.compile("[" + re.escape(r"\`*_{}[]<>()#+-!") + "]")
6
+
7
+
8
+ class Md(str):
9
+ __slots__ = ()
10
+
11
+ def plain(self) -> str:
12
+ return self.replace("\\", "")
13
+
14
+
15
+ class Renderer:
16
+ """Default markdown renderer"""
17
+
18
+ def p(self, text: str) -> str:
19
+ return text + "\n\n"
20
+
21
+ def h(self, level: int, text: str) -> str:
22
+ return f"{'#' * level} {text}\n\n"
23
+
24
+ def br(self) -> str:
25
+ return "<br />\n"
26
+
27
+ def b(self, text: str) -> str:
28
+ return f"**{text}**"
29
+
30
+ def i(self, text: str) -> str:
31
+ return f"*{text}*"
32
+
33
+ def blockquote(self, text: str) -> str:
34
+ return "\n".join([f"> {line}" for line in text.splitlines()]) + "\n\n"
35
+
36
+ def ul(self, lis: Iterable[str]) -> str:
37
+ elts: list[str] = []
38
+ for li in lis:
39
+ lines = li.splitlines()
40
+ if lines:
41
+ elts.append(f"- {lines[0]}")
42
+ elts.extend(f" {line}" for line in lines[1:])
43
+ return "\n".join(elts) + "\n\n"
44
+
45
+ def dl(self, dtdds: Iterable[tuple[str, str]]) -> str:
46
+ return self.ul(self.p(dt) + self.p(dd) for dt, dd in dtdds)
47
+
48
+ def code(self, text: str) -> str:
49
+ return f"`{text}`"
50
+
51
+ def codeblock(self, text: str, lang: str | None = None) -> str:
52
+ return f"```{lang or ''}\n{text}\n```\n\n"
53
+
54
+ def strike(self, text: str) -> str:
55
+ return f"~~~{text}~~~"
56
+
57
+ def hr(self) -> str:
58
+ return "---\n\n"
59
+
60
+ def link(self, url: str, *, text: str | None) -> str:
61
+ if text:
62
+ return f"[{text}]({url})"
63
+ return f"({url})"
64
+
65
+ def e(self, text: str) -> str:
66
+ return _MD_ESCAPE.sub(r"\\\g<0>", text)
67
+
68
+ def body(self, text: str) -> str:
69
+ return text
70
+
71
+ # It's easier to post-process markdown than account for extra newlines while generating
72
+ def postprocess(self, text: str) -> str:
73
+ out = "\n".join(line.rstrip() for line in text.splitlines())
74
+ out = re.sub(r"\n{3,}", "\n\n", out)
75
+ return out
paramspecli/opts.py ADDED
@@ -0,0 +1,324 @@
1
+ from types import EllipsisType
2
+ from typing import Any, Iterable, Literal, overload
3
+
4
+ from .apstub import TypeConverter
5
+ from .fake import Option, RepeatedOption
6
+
7
+
8
+ ##
9
+ @overload
10
+ def option(
11
+ *names: str,
12
+ help: str | Literal[False] | None = None,
13
+ choices: Iterable[str] | None = None,
14
+ metavar: str | None = None,
15
+ show_default: bool | str | None = None,
16
+ ) -> Option[str, None]: ...
17
+
18
+
19
+ ## default: D
20
+ @overload
21
+ def option[D](
22
+ *names: str,
23
+ default: D,
24
+ help: str | Literal[False] | None = None,
25
+ choices: Iterable[str] | None = None,
26
+ metavar: str | None = None,
27
+ show_default: bool | str | None = None,
28
+ ) -> Option[str, D]: ...
29
+
30
+
31
+ ## nargs
32
+ @overload
33
+ def option(
34
+ *names: str,
35
+ nargs: int | Literal["+", "*"],
36
+ help: str | Literal[False] | None = None,
37
+ choices: Iterable[str] | None = None,
38
+ metavar: str | tuple[str, ...] | None = None,
39
+ show_default: bool | str | None = None,
40
+ ) -> Option[list[str], None]: ...
41
+
42
+
43
+ ## optional
44
+ @overload
45
+ def option(
46
+ *names: str,
47
+ nargs: Literal["?"],
48
+ help: str | Literal[False] | None = None,
49
+ choices: Iterable[str] | None = None,
50
+ metavar: str | tuple[str, ...] | None = None,
51
+ show_default: bool | str | None = None,
52
+ ) -> Option[str | EllipsisType, None]: ...
53
+
54
+
55
+ ## nargs, default: D
56
+ @overload
57
+ def option[D](
58
+ *names: str,
59
+ nargs: int | Literal["+", "*"],
60
+ default: D,
61
+ help: str | Literal[False] | None = None,
62
+ choices: Iterable[str] | None = None,
63
+ metavar: str | tuple[str, ...] | None = None,
64
+ show_default: bool | str | None = None,
65
+ ) -> Option[list[str], D]: ...
66
+
67
+
68
+ ## optional, default: D
69
+ @overload
70
+ def option[D](
71
+ *names: str,
72
+ nargs: Literal["?"],
73
+ default: D,
74
+ help: str | Literal[False] | None = None,
75
+ choices: Iterable[str] | None = None,
76
+ metavar: str | tuple[str, ...] | None = None,
77
+ show_default: bool | str | None = None,
78
+ ) -> Option[str | EllipsisType, D]: ...
79
+
80
+
81
+ ## type: T
82
+ @overload
83
+ def option[T](
84
+ *names: str,
85
+ type: TypeConverter[T],
86
+ help: str | Literal[False] | None = None,
87
+ choices: Iterable[T] | None = None,
88
+ metavar: str | None = None,
89
+ show_default: bool | str | None = None,
90
+ ) -> Option[T, None]: ...
91
+
92
+
93
+ ## type: T, default: str | D
94
+ # default is parsed if str, otherwise left as is
95
+ @overload
96
+ def option[T, D](
97
+ *names: str,
98
+ type: TypeConverter[T],
99
+ default: str | D,
100
+ help: str | Literal[False] | None = None,
101
+ choices: Iterable[T] | None = None,
102
+ metavar: str | None = None,
103
+ show_default: bool | str | None = None,
104
+ ) -> Option[T, T | D]: ...
105
+
106
+
107
+ ## type: T, nargs
108
+ @overload
109
+ def option[T](
110
+ *names: str,
111
+ type: TypeConverter[T],
112
+ nargs: int | Literal["+", "*"],
113
+ help: str | Literal[False] | None = None,
114
+ choices: Iterable[T] | None = None,
115
+ metavar: str | tuple[str, ...] | None = None,
116
+ show_default: bool | str | None = None,
117
+ ) -> Option[list[T], None]: ...
118
+
119
+
120
+ ## type: T, optional
121
+ @overload
122
+ def option[T](
123
+ *names: str,
124
+ type: TypeConverter[T],
125
+ nargs: Literal["?"],
126
+ help: str | Literal[False] | None = None,
127
+ choices: Iterable[T] | None = None,
128
+ metavar: str | tuple[str, ...] | None = None,
129
+ show_default: bool | str | None = None,
130
+ ) -> Option[T | EllipsisType, None]: ...
131
+
132
+
133
+ ## type: T, default: str, nargs
134
+ # yes, this one is strange. default is parsed by T, but list is not formed
135
+ @overload
136
+ def option[T](
137
+ *names: str,
138
+ type: TypeConverter[T],
139
+ nargs: int | Literal["+", "*"],
140
+ default: str,
141
+ help: str | Literal[False] | None = None,
142
+ choices: Iterable[T] | None = None,
143
+ metavar: str | tuple[str, ...] | None = None,
144
+ show_default: bool | str | None = None,
145
+ ) -> Option[list[T], T]: ...
146
+
147
+
148
+ ## type: T, default: str, optional
149
+ @overload
150
+ def option[T](
151
+ *names: str,
152
+ type: TypeConverter[T],
153
+ nargs: Literal["?"],
154
+ default: str,
155
+ help: str | Literal[False] | None = None,
156
+ choices: Iterable[T] | None = None,
157
+ metavar: str | tuple[str, ...] | None = None,
158
+ show_default: bool | str | None = None,
159
+ ) -> Option[T | EllipsisType, T]: ...
160
+
161
+
162
+ ## type: T, default: D, nargs
163
+ @overload
164
+ def option[T, D](
165
+ *names: str,
166
+ type: TypeConverter[T],
167
+ nargs: int | Literal["+", "*"],
168
+ default: D,
169
+ help: str | Literal[False] | None = None,
170
+ choices: Iterable[T] | None = None,
171
+ metavar: str | tuple[str, ...] | None = None,
172
+ show_default: bool | str | None = None,
173
+ ) -> Option[list[T], D]: ...
174
+
175
+
176
+ ## type: T, default: D, optional
177
+ @overload
178
+ def option[T, D](
179
+ *names: str,
180
+ type: TypeConverter[T],
181
+ nargs: Literal["?"],
182
+ default: D,
183
+ help: str | Literal[False] | None = None,
184
+ choices: Iterable[T] | None = None,
185
+ metavar: str | tuple[str, ...] | None = None,
186
+ show_default: bool | str | None = None,
187
+ ) -> Option[T | EllipsisType, D]: ...
188
+
189
+
190
+ def option(
191
+ *names: str,
192
+ type: TypeConverter[Any] | None = None,
193
+ default: Any = None,
194
+ nargs: int | Literal["+", "*", "?"] | None = None,
195
+ help: str | bool | None = None,
196
+ choices: Iterable[Any] | None = None,
197
+ metavar: str | tuple[str, ...] | None = None,
198
+ show_default: bool | str | None = None,
199
+ ) -> Option[Any, Any]:
200
+ """Just an option"""
201
+ return Option(
202
+ names,
203
+ help=help,
204
+ type=type,
205
+ nargs=nargs,
206
+ default=default,
207
+ const=... if nargs == "?" else None,
208
+ choices=choices,
209
+ metavar=metavar if metavar is not None else names[0].lstrip("-").upper(),
210
+ action="store",
211
+ #
212
+ hard_show_default=show_default,
213
+ soft_show_default=default is not None,
214
+ )
215
+
216
+
217
+ ##
218
+ @overload
219
+ def repeated_option(
220
+ *names: str,
221
+ help: str | Literal[False] | None = None,
222
+ choices: Iterable[str] | None = None,
223
+ metavar: str | None = None,
224
+ ) -> RepeatedOption[str]: ...
225
+
226
+
227
+ ## nargs
228
+ @overload
229
+ def repeated_option(
230
+ *names: str,
231
+ nargs: int | Literal["+", "*"],
232
+ flatten: Literal[False] = False,
233
+ help: str | Literal[False] | None = None,
234
+ choices: Iterable[str] | None = None,
235
+ metavar: str | tuple[str, ...] | None = None,
236
+ ) -> RepeatedOption[list[str]]: ...
237
+
238
+
239
+ ## nargs, flatten
240
+ @overload
241
+ def repeated_option(
242
+ *names: str,
243
+ nargs: int | Literal["+", "*"],
244
+ flatten: Literal[True],
245
+ help: str | Literal[False] | None = None,
246
+ choices: Iterable[str] | None = None,
247
+ metavar: str | tuple[str, ...] | None = None,
248
+ ) -> RepeatedOption[str]: ...
249
+
250
+
251
+ ## type: T
252
+ @overload
253
+ def repeated_option[T](
254
+ *names: str,
255
+ type: TypeConverter[T],
256
+ flatten: Literal[False] = False,
257
+ help: str | Literal[False] | None = None,
258
+ choices: Iterable[T] | None = None,
259
+ metavar: str | None = None,
260
+ ) -> RepeatedOption[T]: ...
261
+
262
+
263
+ ## type: T, flatten
264
+ # gotcha: type converter should return iterable so it could extend running list
265
+ @overload
266
+ def repeated_option[T](
267
+ *names: str,
268
+ type: TypeConverter[Iterable[T]],
269
+ flatten: Literal[True],
270
+ help: str | Literal[False] | None = None,
271
+ choices: Iterable[T] | None = None,
272
+ metavar: str | None = None,
273
+ ) -> RepeatedOption[T]: ...
274
+
275
+
276
+ ## type: T, nargs
277
+ @overload
278
+ def repeated_option[T](
279
+ *names: str,
280
+ type: TypeConverter[T],
281
+ nargs: int | Literal["+", "*"],
282
+ flatten: Literal[False] = False,
283
+ help: str | Literal[False] | None = None,
284
+ choices: Iterable[T] | None = None,
285
+ metavar: str | tuple[str, ...] | None = None,
286
+ ) -> RepeatedOption[list[T]]: ...
287
+
288
+
289
+ ## type: T, nargs, flatten
290
+ @overload
291
+ def repeated_option[T](
292
+ *names: str,
293
+ type: TypeConverter[T],
294
+ nargs: int | Literal["+", "*"],
295
+ flatten: Literal[True],
296
+ help: str | Literal[False] | None = None,
297
+ choices: Iterable[T] | None = None,
298
+ metavar: str | tuple[str, ...] | None = None,
299
+ ) -> RepeatedOption[T]: ...
300
+
301
+
302
+ def repeated_option(
303
+ *names: str,
304
+ type: TypeConverter[Any] | None = None,
305
+ nargs: int | Literal["+", "*"] | None = None,
306
+ flatten: bool = False,
307
+ help: str | bool | None = None,
308
+ choices: Iterable[Any] | None = None,
309
+ metavar: str | tuple[str, ...] | None = None,
310
+ ) -> RepeatedOption[Any]:
311
+ """Option which could present multiple times on a command line.
312
+ Result is collected into the list"""
313
+ return RepeatedOption(
314
+ names,
315
+ help=help,
316
+ type=type,
317
+ nargs=nargs,
318
+ choices=choices,
319
+ metavar=metavar if metavar is not None else names[0].lstrip("-").upper(),
320
+ action="extend" if flatten else "append",
321
+ default=[],
322
+ #
323
+ soft_show_default=False,
324
+ )
paramspecli/py.typed ADDED
File without changes
paramspecli/util.py ADDED
@@ -0,0 +1,50 @@
1
+ import sys
2
+ from argparse import ArgumentTypeError
3
+ from functools import wraps
4
+ from typing import Callable, NoReturn, TextIO
5
+
6
+ from .apstub import TypeConverter
7
+
8
+
9
+ def echo(*strings: str, nl: bool = True, stream: TextIO | None = None) -> None:
10
+ """Should be used instead of the print() to make intention clear"""
11
+ out = stream or sys.stdout
12
+ for s in strings:
13
+ out.write(s)
14
+ if nl:
15
+ out.write("\n")
16
+ out.flush()
17
+
18
+
19
+ def exit(status: int = 0, message: str | tuple[str, ...] | None = None) -> NoReturn:
20
+ """Print error message and exit"""
21
+ if message:
22
+ if isinstance(message, str):
23
+ message = (message,)
24
+ echo(*message, stream=sys.stderr)
25
+ sys.exit(status)
26
+
27
+
28
+ def catch_all[T](f: TypeConverter[T]) -> TypeConverter[T]:
29
+ """Catch type converter exceptions and present them as the cli errors"""
30
+
31
+ @wraps(f)
32
+ def catcher(s: str) -> T:
33
+ try:
34
+ return f(s)
35
+ except ArgumentTypeError as e:
36
+ raise e
37
+ except Exception as e:
38
+ raise ArgumentTypeError(str(e)) from e
39
+
40
+ return catcher
41
+
42
+
43
+ def resolve_later[**P](resolve: Callable[[], Callable[P, None]]) -> Callable[P, None]:
44
+ """Allows the handler to be resolved at the call time"""
45
+
46
+ @wraps(resolve)
47
+ def wrap(*args: P.args, **kwargs: P.kwargs) -> None:
48
+ resolve()(*args, **kwargs)
49
+
50
+ return wrap