Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,10 @@ uv add yeetr

## Highlights

- **Zero boilerplate** — `yeet app.py` finds and runs `main`, and scaffolds
the file if it doesn't exist yet.
- **Executable shebangs** — `#!yeet` makes a script runnable on its own.
- **Zero boilerplate** — `yeet app.py` finds and runs `main` — or any
public function you name — and scaffolds the file if it doesn't exist yet.
- **Executable shebangs** — `#!yeet` makes a script runnable on its own;
`#!yeet FUNC` picks the function to run.
- **Fully typed** — `str`, `int`, `float`, `bool`, `Path`, `datetime`,
`date`, `time`, `UUID`, `Decimal`, `Literal`, `Enum`, `T | None`,
`list[T]`, tuples, and structured `dataclass`/`NamedTuple` args, all
Expand Down
22 changes: 12 additions & 10 deletions docs/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,25 @@ problem. Quick honest comparison so you can pick the right tool:
| Topic | yeetr | typer |
| --------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| Style | Plain function signature, no decorators | Decorators (`@app.command()`) or `typer.run` |
| Zero-boilerplate runner | `yeet main.py [func] [args...]` script — no `if __name__ == "__main__"` / `yeetr.run(...)` block needed | Always need a `typer.run(...)` call or a decorated `@app.command()` entry point |
| 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 |
| 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(...)` |
| Zero-boilerplate runner | `yeet main.py [func] [args...]` script — no `if __name__ == "__main__"` / `yeetr.run(...)` block needed | Similar: `typer main.py run [args...]` runs scripts, even ones that don't import typer; in code you need `typer.run(...)` or an `@app.command()` entry point |
| Executable shebang | `#!yeet` or `#!uv run yeet` can make the script itself executable without extra wrapper code; `#!yeet FUNC` dispatches to a specific function | No shebang equivalent: the `typer` command needs its `run` subcommand between the file and the args, which a shebang can't inject |
| Arg vs. option mapping | Python's `*` separator decides: before `*` = positional args, after `*` = `--options` | Presence of a default decides: no default = positional arg, default = `--option`; override per parameter via `typer.Argument(...)` / `typer.Option(...)` |
| Per-param metadata | `Annotated[T, Arg(...)]` / `Annotated[T, Opt(...)]` | `Annotated[T, typer.Argument(...)]` / `typer.Option(...)` |
| Custom parsers | `Arg/Opt(parser=...)` — the CLI string is first coerced to the parser's annotated input type (path validators compose), then the parser runs | `typer.Argument/Option(parser=...)` — the parser receives the raw string |
| Structured arg object | Accept one `dataclass` or `NamedTuple`; fields become the CLI and the function receives the object | Declare command params individually, then construct your object inside the command |
| Variadic positional args | Native `*args: T` maps to a trailing variadic positional arg | Use `list[T]` with `typer.Argument(...)` |
| Boolean flags | Default drives the flag: `= False` -> `--flag`, `= True` -> `--no-flag` | Pair of flags declared explicitly: `--flag / --no-flag` |
| Subcommands | Not supported (single command per script) | First-class subcommands, command groups, nested apps |
| Async functions | Native: `async def` is run via `asyncio.run` / `uvloop.run` | Not built-in; wrap with `asyncio.run(...)` yourself |
| Boolean flags | One flag, driven by the default: `= False` -> `--flag`, `= True` -> `--no-flag` | Auto-generates the pair `--flag / --no-flag`; explicit declaration only to customize the names |
| Subcommands | Subcommand-style dispatch via the runner: `yeet main.py FUNC [args...]` picks any public function by name (also `yeet FILE:FUNC` and `#!yeet FUNC` shebangs); no command groups, nesting, or function listing in `--help` | First-class subcommands, command groups, nested apps |
| Async functions | Native: `async def` is run via `asyncio.run` / `uvloop.run` | Not built-in; an `async def` command is silently never awaited — wrap with `asyncio.run(...)` yourself |
| Shell completion | Not built-in | Built-in (bash/zsh/fish/PowerShell) |
| Help rendering | Rich tables for args and options | Rich-formatted help via `rich` |
| Type-checker friendliness | Designed to be Pyright-strict clean end-to-end | Some patterns require `# type: ignore` under strict settings |
| Type-checker friendliness | Pyright-strict clean end-to-end; `Arg`/`Opt` are plain, precisely-typed dataclasses | Also clean under strict Pyright; `typer.Argument()`/`typer.Option()` return `Any`, which relaxes checking at the parameter site |
| Logging | Rich logging set up by default (opt-out) | Not opinionated about logging |
| Dependencies | `rich`, `rich-argparse` (small footprint) | `click`, `rich`, `shellingham`, `typing-extensions` |
| Maturity / ecosystem | New and small | Widely adopted, large ecosystem |
| Best for | Single-purpose scripts and tools where the function *is* the CLI | Multi-command CLIs, distributed apps, anything needing completion |

If you need subcommands or shell completion, use typer. If you want one
function = one CLI with minimal ceremony and strict typing, yeetr is
designed for that.
If you need nested command groups or shell completion, use typer. If you
want one function = one CLI with minimal ceremony and strict typing — with
`yeet FILE FUNC` covering the simple multi-command case — yeetr is designed
for that.
3 changes: 3 additions & 0 deletions docs/guide/cli-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@
- `Opt(hidden=True)` still parses but is hidden from `--help`.
- `exists`/`file_okay`/`dir_okay`/`readable`/`writable` validate `Path`
parameters (and `Path`-typed lists, tuples, and `*args`) at parse time.
- `Arg(parser=...)`/`Opt(parser=...)` run a custom converter: the CLI string
is coerced to the parser's annotated input type, then the parser produces
the final value. Not supported on `list`/`tuple` parameters or `*args`.
28 changes: 22 additions & 6 deletions docs/guide/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,37 @@ script for you, mark it executable, and print the created path. Run the
same command a second time, or call `./app.py` directly, and it will
execute normally.

The default function name is `main`. Pass a different one to pick another
top-level function in the same file:
The default function name is `main`. To run another function, name it — the
target must be a **public** `def`/`async def` defined in that file (not an
import, a class, or a `_`-prefixed helper). Decorated functions qualify only
if the decorator preserves the function's identity with `functools.wraps` —
otherwise the name is not recognised as a runnable function:

```python
# app.py
def main(...) -> None: ...
def main(thing: int) -> None: ...
def greet(name: str, *, loud: bool = False) -> None: ...
```

```bash
yeet app.py greet world --loud
```

Because the `.py` file identifies itself, the function can also come first or
be attached with a colon — pick whichever reads best:

```bash
yeet greet app.py world --loud # function first
yeet app.py:greet world --loud # FILE:FUNC
```

If `main` takes a **string** first argument, a bare `yeet app.py greet` is
genuinely ambiguous — `greet` could be the function to run *or* a value for
`main` — so yeetr raises instead of guessing. Disambiguate with
`yeet app.py main greet` (pass it to `main`) or the `app.py:greet` /
function-first forms (run `greet`).

`yeet app.py --help` prints the **target function's** help, not yeet's.
`yeet` itself only has `yeet FILE [FUNC] [args...]`.

You can still use the explicit `yeetr.run(main)` form when you prefer —
the `yeet` script is just sugar on top of it.
Expand Down Expand Up @@ -95,8 +111,8 @@ chmod +x greet.py
./greet.py world --loud
```

If you need a different entry function, keep the shebang simple and call
`uv run yeet app.py other_func ...` explicitly instead.
To make the script run a function other than `main`, name it in the shebang
`#!yeet greet` — and running `./greet.py ...` invokes that function directly.

## Next steps

Expand Down
46 changes: 44 additions & 2 deletions docs/guide/parameter-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ yeet app.py input.pdf -w 8
```

`Arg` accepts `help`, `metavar`, `min` (only meaningful on [variadic `*args`](parameter-types.md#variadic-positional-args-args)),
and the [path validators](path-validators.md). `Opt` accepts `alias`,
`aliases`, `help`, `metavar`, `envvar`, `hidden`, and the
`parser`, and the [path validators](path-validators.md). `Opt` accepts
`alias`, `aliases`, `help`, `metavar`, `envvar`, `hidden`, `parser`, and the
[path validators](path-validators.md) too. Mixing them (e.g. `Opt` on a
positional or `Arg` on a keyword-only parameter) raises a clear `YeetrError`.

Expand Down Expand Up @@ -75,6 +75,48 @@ Env-var values are type-coerced just like CLI values. `bool` accepts
(`:` on POSIX, `;` on Windows). `tuple[...]` also splits on `os.pathsep`.
`Literal` and enum choices are validated.

## Custom parsers (`parser=`)

`parser=` supplies your own converter for a parameter, so yeetr can handle
domain types it doesn't build in. yeetr inspects the parser's single
(annotated) parameter, coerces the raw CLI string to *that* type with its
normal machinery, then calls the parser to produce the value passed to the
function:

```python
from pathlib import Path
from typing import Annotated
from yeetr import Opt


def read_bytes(path: Path) -> bytes:
return path.read_bytes()


def main(*, pdf_bytes: Annotated[bytes, Opt(parser=read_bytes)]) -> None:
...
```

```bash
yeet app.py --pdf-bytes input.pdf
```

Here yeetr sees `read_bytes` takes a `Path`, coerces `"input.pdf"` to a
`Path`, and calls `read_bytes` with it. The outer annotation (`bytes`) is
only the function-facing type — yeetr never coerces to it; the parser is
trusted for the output.

The parser's input type drives everything user-facing: `--help` shows the
input type, and the [path validators](path-validators.md) compose —
`Opt(parser=read_bytes, exists=True)` validates the intermediate `Path`
before the parser runs. If the parser raises `ValueError` or `TypeError`,
the failure is reported as a normal argument error.

The parser must be a callable taking exactly one type-annotated parameter;
anything else raises `YeetrError` at parser-build time. `parser=` is not
supported on `list`/`tuple` parameters or variadic `*args`, and `bool`,
`list`, and `tuple` parser input types are rejected.

## Hidden options (`Opt(hidden=True)`)

Hidden options still parse from the CLI but are absent from `--help` (both
Expand Down
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ Parameters **before** the bare `*` become positional CLI args. Parameters
## Where to go next

- [Getting Started](guide/getting-started.md) — installation, the `yeet`
script, the explicit `yeetr.run(main)` form, and executable shebangs.
script and how it picks the function to run, the explicit `yeetr.run(main)`
form, and executable shebangs.
- [Async Support](guide/async.md) — `async def main`, handled natively.
- [Parameter Types](guide/parameter-types.md) — every type yeetr understands,
from `Path` to `Enum` to tuples and variadic `*args`.
Expand Down
Loading