yeetr 2026.5.21.post15__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Roger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,480 @@
1
+ Metadata-Version: 2.4
2
+ Name: yeetr
3
+ Version: 2026.5.21.post15
4
+ Summary: A tiny, typed, signature-driven CLI runner.
5
+ Keywords: python
6
+ Author: yeeter
7
+ Author-email: yeeter <noreply@example.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Dist: rich>=15.0.0
16
+ Requires-Dist: rich-argparse>=1.5.2
17
+ Requires-Dist: uvloop>=0.21.0 ; extra == 'uvloop'
18
+ Requires-Python: >=3.14
19
+ Project-URL: Homepage, https://rogerthomas.github.io/yeetr/
20
+ Project-URL: Repository, https://github.com/RogerThomas/yeetr
21
+ Project-URL: Documentation, https://rogerthomas.github.io/yeetr/
22
+ Provides-Extra: uvloop
23
+ Description-Content-Type: text/markdown
24
+
25
+ <p align="center">
26
+ <img src="assets/yeetr.png" alt="yeeter" width="500">
27
+ </p>
28
+
29
+ # yeeter
30
+
31
+ [![Release](https://img.shields.io/github/v/release/RogerThomas/yeetr)](https://github.com/RogerThomas/yeetr/releases)
32
+ [![Build](https://img.shields.io/github/actions/workflow/status/RogerThomas/yeetr/main.yml?branch=main)](https://github.com/RogerThomas/yeetr/actions/workflows/main.yml?query=branch%3Amain)
33
+ [![License](https://img.shields.io/github/license/RogerThomas/yeetr)](https://github.com/RogerThomas/yeetr/blob/main/LICENSE)
34
+
35
+ A tiny, typed, signature-driven CLI runner.
36
+
37
+ PyPI distribution: `yeetr`
38
+ Python import package: `yeeter`
39
+ CLI command: `yeet`
40
+
41
+ > No decorators.
42
+ > No command classes.
43
+ > No ceremony.
44
+ > Just yeet the function.
45
+
46
+ ---
47
+
48
+ ## Minimal example
49
+
50
+ ### Zero-boilerplate: the `yeet` script
51
+
52
+ Installing yeeter also installs a `yeet` script that finds and runs a
53
+ function in any Python file.
54
+
55
+ No `if __name__ == "__main__"` block, no `yeeter.run(...)` call — just
56
+ the function:
57
+
58
+ ```python
59
+ # app.py
60
+ def main(thing: int, *, n: float = 0.1) -> None:
61
+ print(thing, n)
62
+ ```
63
+
64
+ ```
65
+ yeet file.py 5 --n 0.2
66
+ ```
67
+
68
+ The default function name is `main`. Pass a different one to pick another
69
+ top-level function in the same file:
70
+
71
+ ```python
72
+ # app.py
73
+ def main(...) -> None: ...
74
+ def greet(name: str, *, loud: bool = False) -> None: ...
75
+ ```
76
+
77
+ ```
78
+ yeet file.py greet world --loud
79
+ ```
80
+
81
+ `yeet file.py --help` prints the **target function's** help, not yeet's.
82
+ `yeet` itself only has `yeet FILE [FUNC] [args...]`.
83
+
84
+ You can still use the explicit `yeeter.run(main)` form when you prefer —
85
+ the `yeet` script is just sugar on top of it.
86
+
87
+ ### Explicit `yeeter.run(main)`
88
+
89
+ ```python
90
+ def main(thing: int, *, n: float = 0.1) -> None:
91
+ print(thing, n)
92
+
93
+
94
+ if __name__ == "__main__":
95
+ import yeeter
96
+ yeeter.run(main)
97
+ ```
98
+
99
+ ```
100
+ yeet file.py 5 --n 0.2
101
+ ```
102
+
103
+ Note the bare `*` in the signature: parameters **before** it become
104
+ positional CLI args, parameters **after** it become `--options`. That's
105
+ the whole mapping — no decorators, no per-parameter annotations needed.
106
+
107
+ ---
108
+
109
+ ## Hashbang
110
+
111
+ For tiny scripts, you can make the file itself executable and let `yeet`
112
+ discover `main` directly from the shebang. The short forms are:
113
+
114
+ ```python
115
+ #!yeet
116
+ ```
117
+
118
+ or:
119
+
120
+ ```python
121
+ #!uv run yeet
122
+ ```
123
+
124
+ For example:
125
+
126
+ ```python
127
+ #!yeet
128
+
129
+ def main(name: str, *, loud: bool = False) -> None:
130
+ print(name.upper() if loud else name)
131
+ ```
132
+
133
+ Then run it directly:
134
+
135
+ ```
136
+ chmod +x greet.py
137
+ ./greet.py world --loud
138
+ ```
139
+
140
+ If you need a different entry function, keep the shebang simple and call
141
+ `uv run yeet file.py other_func ...` explicitly instead.
142
+
143
+ ---
144
+
145
+ ## Async
146
+
147
+ ```python
148
+ async def main(name: str, *, loud: bool = False) -> None:
149
+ ...
150
+ ```
151
+
152
+ ```
153
+ yeet file.py world --loud
154
+ ```
155
+
156
+ If the function is a coroutine, its result is awaited via `asyncio.run`,
157
+ or via [`uvloop.run`](https://github.com/MagicStack/uvloop) when the
158
+ optional `uvloop` extra is installed:
159
+
160
+ ```
161
+ uv add "yeeter[uvloop]"
162
+ ```
163
+
164
+ When `uvloop` is importable, yeeter uses it transparently — no code
165
+ change required. Otherwise it falls back to the stdlib event loop.
166
+
167
+ ---
168
+
169
+ ## Path
170
+
171
+ ```python
172
+ from pathlib import Path
173
+
174
+
175
+ def main(path: Path, *, output: Path | None = None) -> None:
176
+ ...
177
+ ```
178
+
179
+ ```
180
+ yeet file.py input.pdf --output out.txt
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Literal choices
186
+
187
+ ```python
188
+ from typing import Literal
189
+
190
+
191
+ def main(*, format: Literal["json", "csv"] = "json") -> None:
192
+ ...
193
+ ```
194
+
195
+ ```
196
+ yeet file.py --format csv
197
+ ```
198
+
199
+ ---
200
+
201
+ ## `Arg` and `Opt` metadata
202
+
203
+ For aliases and help text, use `Arg` (positional) or `Opt` (keyword-only)
204
+ inside `Annotated`:
205
+
206
+ ```python
207
+ from pathlib import Path
208
+ from typing import Annotated
209
+ from yeeter import Arg, Opt
210
+
211
+
212
+ def main(
213
+ path: Annotated[Path, Arg(help="Input file")],
214
+ *,
215
+ workers: Annotated[int, Opt(alias="-w", help="Worker count")] = 4,
216
+ ) -> None:
217
+ ...
218
+ ```
219
+
220
+ ```
221
+ yeet file.py input.pdf -w 8
222
+ ```
223
+
224
+ `Arg` accepts `help`, `metavar`, `min`, and the path validators below. `Opt`
225
+ accepts `alias`, `aliases`, `help`, `metavar`, `envvar`, `hidden`, and the
226
+ path validators below. Mixing them (e.g. `Opt` on a positional or `Arg` on a
227
+ keyword-only parameter) raises a clear `YeeterError`.
228
+
229
+ You can also define aliases once and reuse them:
230
+
231
+ ```python
232
+ from pathlib import Path
233
+ from typing import Annotated
234
+ from yeeter import Arg, Opt
235
+
236
+
237
+ type InputPath = Annotated[Path, Arg(help="Input file")]
238
+ type WorkerCount = Annotated[int, Opt(alias="-w", help="Worker count")]
239
+
240
+
241
+ def main(path: InputPath, *, workers: WorkerCount = 4) -> None:
242
+ ...
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Environment variable fallback (`Opt(envvar=...)`)
248
+
249
+ `Opt(envvar="NAME")` falls back to an environment variable when the flag is
250
+ not provided on the CLI. Precedence: **explicit CLI > env var > default**.
251
+
252
+ ```python
253
+ from typing import Annotated
254
+ from yeeter import Opt
255
+
256
+
257
+ def main(*, workers: Annotated[int, Opt(envvar="WORKERS")] = 4) -> None:
258
+ ...
259
+ ```
260
+
261
+ ```
262
+ WORKERS=8 yeet file.py # workers == 8
263
+ yeet file.py --workers 16 # workers == 16 (CLI wins)
264
+ yeet file.py # workers == 4 (default)
265
+ ```
266
+
267
+ Env-var values are type-coerced just like CLI values. `bool` accepts
268
+ `1/0/true/false/yes/no` (case-insensitive). `list[T]` splits on `os.pathsep`
269
+ (`:` on POSIX, `;` on Windows). `Literal` choices are validated.
270
+
271
+ ---
272
+
273
+ ## Hidden options (`Opt(hidden=True)`)
274
+
275
+ Hidden options still parse from the CLI but are absent from `--help` (both
276
+ the usage line and the options table):
277
+
278
+ ```python
279
+ from typing import Annotated
280
+ from yeeter import Opt
281
+
282
+
283
+ def main(*, debug: Annotated[bool, Opt(hidden=True)] = False) -> None:
284
+ ...
285
+ ```
286
+
287
+ ---
288
+
289
+ ## Path validators
290
+
291
+ `Arg` and `Opt` accept `exists`, `file_okay`, `dir_okay`, `readable`, and
292
+ `writable` for `Path` parameters. They run at parse time and fail with a
293
+ clear error:
294
+
295
+ ```python
296
+ from pathlib import Path
297
+ from typing import Annotated
298
+ from yeeter import Arg
299
+
300
+
301
+ def main(
302
+ src: Annotated[Path, Arg(exists=True, dir_okay=False, readable=True)],
303
+ dst: Annotated[Path, Arg(writable=True)],
304
+ ) -> None:
305
+ ...
306
+ ```
307
+
308
+ Defaults mirror typer: `file_okay=True`, `dir_okay=True`, others off.
309
+ Setting any path-check on a non-`Path` parameter raises `YeeterError` at
310
+ parser-build time. Validators also apply to `list[Path]` and to
311
+ `*paths: Path`.
312
+
313
+ ---
314
+
315
+ ## Variadic positional args (`*args`)
316
+
317
+ `*args` maps to a trailing variadic positional CLI argument. The annotation
318
+ on `*args` is the **element type** (not `list[T]`):
319
+
320
+ ```python
321
+ from pathlib import Path
322
+
323
+
324
+ def main(dst: Path, *sources: Path) -> None:
325
+ ...
326
+ ```
327
+
328
+ ```
329
+ yeet file.py dst src1 src2 src3
330
+ ```
331
+
332
+ By default `*args` accepts zero or more values (argparse `nargs="*"`). Use
333
+ `Arg(min=1)` to require at least one:
334
+
335
+ ```python
336
+ from typing import Annotated
337
+ from yeeter import Arg
338
+
339
+
340
+ def main(*sources: Annotated[Path, Arg(min=1, help="Source paths")]) -> None:
341
+ ...
342
+ ```
343
+
344
+ Keyword-only options remain `--flags` after `*args`. `**kwargs` is not
345
+ supported.
346
+
347
+ **Why `Annotated`?** Python's type system only permits call expressions
348
+ (`Opt(...)`) inside the metadata slot of `Annotated`. No other syntax is
349
+ accepted by Pyright in strict mode. The `Annotated` form is verbose but is
350
+ the only way to attach per-parameter metadata that fully type-checks.
351
+
352
+ ---
353
+
354
+ ## Rules
355
+
356
+ - **Positional** parameters become positional CLI args.
357
+ - **Keyword-only** parameters (after `*`) become `--options`.
358
+ - Names convert from `snake_case` to `kebab-case` for CLI flags.
359
+ - `flag: bool = False` becomes `--flag`.
360
+ - `flag: bool = True` becomes `--no-flag`.
361
+ - Required `bool` parameters raise a clear error.
362
+ - `T | None` / `Optional[T]` are accepted; treated as their inner type with
363
+ `None` as default.
364
+ - `list[T]` becomes a repeated option (`--tag a --tag b`).
365
+
366
+ ---
367
+
368
+ ## Supported types
369
+
370
+ `str`, `int`, `float`, `bool`, `pathlib.Path`, `typing.Literal[...]`,
371
+ `T | None`, `list[T]`. Anything else raises a clear `YeeterError`.
372
+
373
+ ---
374
+
375
+ ## Logging
376
+
377
+ By default, `yeeter.run` installs a Rich-based logging handler before
378
+ invoking your function, so you get formatted logs with zero boilerplate:
379
+
380
+ ```python
381
+ import logging
382
+
383
+ import yeeter
384
+
385
+ logger = logging.getLogger("app")
386
+
387
+
388
+ def main(thing: int) -> None:
389
+ logger.info("thing = %s", thing)
390
+ ```
391
+
392
+ If your function has a `log_level` parameter (e.g.
393
+ `log_level: Literal["debug", "info", "warning", "error"] = "info"`), its
394
+ value drives the log level. Otherwise, the default is `INFO`.
395
+
396
+ Setup is idempotent: if the root logger already has handlers, yeeter does
397
+ not touch them. To take full control of logging yourself, opt out:
398
+
399
+ ```python
400
+ yeeter.run(main, should_setup_logging=False)
401
+ ```
402
+
403
+ ---
404
+
405
+ ## Testing
406
+
407
+ `run()` accepts an explicit `argv` for tests:
408
+
409
+ ```python
410
+ yeeter.run(main, argv=["5", "--n", "0.2"])
411
+ ```
412
+
413
+ ---
414
+
415
+ ## yeeter vs. typer
416
+
417
+ [Typer](https://github.com/fastapi/typer) is a mature, feature-rich CLI
418
+ framework and a direct inspiration for yeeter — the `Annotated[..., Arg/Opt]`
419
+ metadata pattern, path validators, and envvar fallback all take cues from
420
+ typer. yeeter is a much smaller library aimed at a narrower slice of the
421
+ problem. Quick honest comparison so you can pick the right tool:
422
+
423
+ | Topic | yeeter | typer |
424
+ | -------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ |
425
+ | Style | Plain function signature, no decorators | Decorators (`@app.command()`) or `typer.run` |
426
+ | Zero-boilerplate runner | `yeet main.py [func] [args...]` script — no `if __name__ == "__main__"` / `yeeter.run(...)` block needed | Always need a `typer.run(...)` call or a decorated `@app.command()` entry point |
427
+ | Executable shebang | `#!yeet` or `#!uv run yeet` can make the script itself executable without extra wrapper code | No equivalent single-line signature-driven runner; still need a `typer.run(...)` or app entry point |
428
+ | Arg vs. option mapping | Uses Python's `*` separator: before `*` = positional args, after `*` = `--options` (no per-param annotation needed) | Decide per parameter via `typer.Argument(...)` / `typer.Option(...)` |
429
+ | Per-param metadata | `Annotated[T, Arg(...)]` / `Annotated[T, Opt(...)]` | `Annotated[T, typer.Argument(...)]` / `typer.Option(...)` |
430
+ | Variadic positional args | Native `*args: T` maps to a trailing variadic positional arg | Use `list[T]` with `typer.Argument(...)` |
431
+ | Boolean flags | Default drives the flag: `= False` -> `--flag`, `= True` -> `--no-flag` | Pair of flags declared explicitly: `--flag / --no-flag` |
432
+ | Subcommands | Not supported (single command per script) | First-class subcommands, command groups, nested apps |
433
+ | Async functions | Native: `async def` is run via `asyncio.run` / `uvloop.run` | Not built-in; wrap with `asyncio.run(...)` yourself |
434
+ | Shell completion | Not built-in | Built-in (bash/zsh/fish/PowerShell) |
435
+ | Help rendering | Rich tables for args and options | Rich-formatted help via `rich` |
436
+ | Type-checker friendliness | Designed to be Pyright-strict clean end-to-end | Some patterns require `# type: ignore` under strict settings |
437
+ | Logging | Rich logging set up by default (opt-out) | Not opinionated about logging |
438
+ | Dependencies | `rich`, `rich-argparse` (small footprint) | `click`, `rich`, `shellingham`, `typing-extensions` |
439
+ | Maturity / ecosystem | New and small | Widely adopted, large ecosystem |
440
+ | Best for | Single-purpose scripts and tools where the function *is* the CLI | Multi-command CLIs, distributed apps, anything needing completion |
441
+
442
+ If you need subcommands or shell completion, use typer. If you want one
443
+ function = one CLI with minimal ceremony and strict typing, yeeter is
444
+ designed for that.
445
+
446
+ ---
447
+
448
+ ## Releases
449
+
450
+ `yeeter` uses CalVer based on the release date. Versions are published in
451
+ PEP 440 canonical form as `YYYY.M.D`, so a release on 2026-05-21 is
452
+ `2026.5.21`; multiple releases on the same day use `.postN`, for example
453
+ `2026.5.21.post1`.
454
+
455
+ Run `task release` to create the `release/{TAG}` PR, then merge it.
456
+ Then create and push the matching release tag. GitHub Actions validates
457
+ the tag, creates the GitHub Release, and a separate workflow deploys
458
+ docs.
459
+
460
+ If you need to bypass the PR flow, run `task release-direct`. That bumps
461
+ the version on `main`, runs `task deps-lock`, commits, pushes `main`,
462
+ creates the matching tag, and pushes the tag.
463
+
464
+ To bump a release version manually, run `uv version <version>`.
465
+
466
+ Install from PyPI with:
467
+
468
+ ```bash
469
+ pip install yeetr
470
+ ```
471
+ ---
472
+
473
+ ## Development
474
+
475
+ ```
476
+ uv sync
477
+ uv run ruff check
478
+ uv run pyright
479
+ uv run pytest
480
+ ```