Metadata-Version: 2.5
Name: atomm-clap
Version: 0.1.9
Summary: A command-line parser for building deep, complex, readable, memorable CLIs.
Project-URL: Homepage, https://github.com/johannes-bauer/atomm-clap
Author-email: Johannes Bauer <atomm-clap@interesting-problems.de>
License: MIT
License-File: LICENSE
Keywords: argument-parser,cli,subcommands
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# atomm-clap

**Almost Too Much Magic Command Line Argument Parser** — a Python library for
building deep, domain-specific CLIs whose command definitions read like a list
of example invocations.

---

## The idea

Most CLI parsers build a tree of flags and subcommands and then map that tree
onto function calls. atomm-clap works the other way around: you write the
command paths you want the user to type, and the library figures out the
routing.

```python
infra.server.by.nickname[SERVER].edit.config(handler, SERVER)
infra.server.by.nickname[SERVER].restart(handler, SERVER)
infra.deployment[DEPLOYMENT].add.probe.type_[TYPE].to.microscope[SCOPE](handler, ...)
```

This makes atomm-clap a good fit for **infrastructure tools, device managers,
and any CLI where the commands have a rich, noun-verb domain vocabulary** —
the kind of thing kubectl and the Docker CLI do, where commands read like 
sentences.  atomm-clap could, for example, easily implement

```bash
kubectl list <entity> in namespace <namespace> with label <label>
```

instead of
```bash
kubectl get pods -n <namespace> -l <label-key>=<label-value>
```

---

## Installation

```bash
pip install atomm-clap
```

Or from source:

```bash
git clone https://github.com/johannes-bauer/atomm-clap
cd atomm-clap
pip install -e .
```

---

## Quickstart

```python
#!/usr/bin/env python3
import sys
from atomc import CLI, parse_tokens, Argument

NAME = Argument('name')

cli = CLI('greeter')
cli.hello(lambda: print("Hello, world!"))
cli.hello.to[NAME](lambda n: print(f"Hello, {n}!"), NAME)

parse_tokens(cli, sys.argv)
```

```
$ greeter hello
Hello, world!

$ greeter hello to Alice
Hello, Alice!

$ greeter --help
Usage:
   greeter hello
   greeter hello to [name]
```

---

## Building a CLI

### Paths

A CLI is a `CLI` object. Attribute access on it creates subcommand nodes.
Calling a node registers a handler for that path:

```python
cli = CLI('mytool')
cli.server.list(list_servers)          # mytool server list
cli.server.restart(restart_server)     # mytool server restart
```

How matched values are delivered to handlers is covered below.

### Arguments

`Argument` defines a positional capture node. It matches any token and records
the value. Use square-bracket syntax to place it in the path:

```python
SERVER = Argument('server')
PORT   = Argument('port', arg_type=int)
```

`arg_type` restricts matching to tokens that can be converted and delivers the
converted value to the handler:

```python
COUNT = Argument('count', arg_type=int)
cli.run[COUNT].times(handler, COUNT)   # only integers match; handler receives an int
```

### Handlers

Handlers are plain callables — functions or lambdas. There are two ways to
pass matched `Argument` values to a handler.

**Explicit:** list `Argument` objects after the callable. The function
parameter names can be anything:

```python
cli.status(lambda: print("OK"))

SERVER = Argument('server')
PORT   = Argument('port', arg_type=int)

def show_server(srv):
    print(f"Showing {srv}")

cli.server[SERVER].show(show_server, SERVER)               # mytool server <name> show
cli.server[SERVER].set.port[PORT](set_port, SERVER, PORT)  # mytool server <name> set port <n>
```

**Inferred:** pass the handler with `call=` and name each parameter to match
the corresponding `Argument` name. atomm-clap reads the parameter names with
`inspect` and maps them automatically:

```python
def show_server(server):    # 'server' matches Argument('server')
    print(f"Showing {server}")

cli.server[SERVER].show(call=show_server)
```

Both forms handle multiple arguments the same way:

```python
ENV     = Argument('env')
VERSION = Argument('version')

def do_deploy(env, version): ...

cli.deploy[ENV].version[VERSION](do_deploy, ENV, VERSION)   # explicit
cli.deploy[ENV].version[VERSION](call=do_deploy)             # inferred
```

Prefer `call=` when parameter names naturally match `Argument` names — it
keeps path definitions concise and avoids repeating `Argument` objects.

### Accessing matched values directly

As a lower-level alternative, a handler can accept the raw parser state and
call `ARG.get_value(parser_state)` to retrieve values:

```python
def handler(parser_state):
    server = SERVER.get_value(parser_state)
```

This is rarely needed — the explicit and `call=` forms cover the common cases.

---

## Shell completion

Every `CLI` automatically gains a hidden `completion` subcommand. Source the
output in your shell profile to enable tab completion.

### bash

```bash
# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
eval "$(python path/to/my_tool/cli.py completion bash mytool)" 
```

Or add to `~/.bashrc`:

```bash
# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
source <(python path/to/my_tool/cli.py completion bash mytool)
```

### zsh

```zsh
# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
eval "$(python path/to/my_tool/cli.py completion zsh mytool)"
```

### fish

```fish
# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
python path/to/my_tool/cli.py completion fish mytool | source
```

Or save to the completions directory:

```fish
# Creates the function 'mytool'.  Pass anything instead of 'mytool' to choose a different name.
python path/to/my_tool.py completion fish mytool > ~/.config/fish/completions/mytool.fish
```

The completion scripts invoke the tool itself with a special sentinel token,
so completion always reflects the live CLI definition — no separate completion
file to maintain.

---

## Help text

Every node responds to `--help` / `-h`. Descriptions can be provided as a
string or inferred from a function's docstring:

```python
cli.server(description="Manage servers")

def do_restart(server):
    """Restart the named server and wait for it to come back online."""
    ...

cli.server[SERVER].restart(do_restart, SERVER)
```

```
$ mytool server --help
Manage servers

Usage:
   mytool server <server> restart
   
$ mytool server "Some Server" restart --help
Restart the named server and wait for it to come back online.

Usage:
   mytool server <server> restart
```

---

## Example: bridge.py

`examples/bridge.py` is a self-contained starship bridge simulator that
demonstrates the patterns atomm-clap was built for: deep paths, named-entity
selection, two lookup routes to the same action, and optional mid-path
segments.

```
bridge status
bridge alert set red
bridge system sensors repair
bridge system shields set power 80
bridge shields raise
bridge engine engage warp 6
bridge weapon by type torpedo fire at target "Klingon Bird-of-Prey"
bridge weapon by type phaser status
bridge crew ranked "chief engineer" assign to system weapons
bridge crew named Reyes report
bridge scan for ships
bridge scan sector "12-A" for resources
bridge course plot to system "Alpha Centauri"
bridge log entry add "Encountered anomaly at bearing 270"
bridge log show
```

The CLI definition (excerpt) shows how the paths look in code:

```python
# Two lookup routes ("named" and "ranked") leading to the same actions
bridge.crew.named[CREW_NAME].report(cmd_crew_report_by_name, CREW_NAME)
bridge.crew.named[CREW_NAME].assign.to.system[SYSTEM](cmd_crew_assign_by_name, CREW_NAME, SYSTEM)
bridge.crew.ranked[CREW_RANK].report(cmd_crew_report_by_rank, CREW_RANK)
bridge.crew.ranked[CREW_RANK].assign.to.system[SYSTEM](cmd_crew_assign_by_rank, CREW_RANK, SYSTEM)

# "sector <X>" is optional — both forms route to the same handler
bridge.scan.for_.ships(cmd_scan_ships, SECTOR)
bridge.scan.for_.resources(cmd_scan_resources, SECTOR)
bridge.scan.sector[SECTOR].for_.ships(cmd_scan_ships, SECTOR)
bridge.scan.sector[SECTOR].for_.resources(cmd_scan_resources, SECTOR)

# Eight tokens deep
bridge.weapon.by.type_[WEAPON_TYPE].fire.at.target[TARGET](cmd_weapon_fire, WEAPON_TYPE, TARGET)
```

Run it with:

```bash
cd examples
PYTHONPATH=../src python3 bridge.py --help
```


# Hints

## Optional `--flag`s and `--option`s
atomm-clap treats the whole command line as one sentence in a DSL—the only distinction it makes between tokens is 
between `Subcommand`s and `Argument`s.  `--flags`- and `--option`-like patterns can be implemented within that framework:

### Implement `--flag`s:

```python
### CLI DEFINITION
def tell_me_about_frogs(everything):
    if not everything:
        assert everything is None
        print("Oh, where do I even start!")
    else:
        assert everything == '--everything'
        print("Really, everything?")
        
tell = CLI('tell')
tell.me.about.frogs(tell_me_about_frogs, tell.me['--everything'])
tell.me['--everything'].about = tell.me.about
```

The `+=` operator merges one node's successors and executable into another,
creating an optional segment in the path.

```bash
### SHELL USAGE 
$ tell me about frogs
Oh, where do I even start!

$ tell me --everything about frogs
Really, everything?
```

### Implement `--option OPTIONAL_ARGUMENT`:
```python
### CLI DEFINITION
counting = CLI('counting')

SEVEN = Argument('seven')

def did_it(seven):
    if not seven:
        print('Skroob: Six? What happened to eight and seven?')
    elif seven not in ['seven', '7']:
        print(f"{seven}?!?")
    else:
        print("Computer: ... five, four, three, two, one.  Have a nice day.\nEverybody: Thank you!")

counting.down.ten.nine.six(did_it, SEVEN)
counting.down.ten.nine['--eight'][SEVEN] += counting.down.ten.nine
```

```bash
### SHELL USAGE
$ counting down ten nine six
Skroob: Six? What happened to eight and seven?

$ counting down ten nine --eight whatever six
whatever?!?

$ counting down ten nine --eight seven six
Computer: ... five, four, three, two, one.  Have a nice day.
Everybody: Thank you!
```

## Python reserved words

Some Python keywords appear naturally in CLI paths. Append `_` to use them;
atomm-clap strips it from the matched token:

| Write | Matches |
|-------|---------|
| `cli.list_` or `cli.for_` | `list`, `for` |
| `cli.type_` | `type` |
| `cli.raise_` | `raise` |

```python
cli.scan.for_.ships(scan_ships)   # mytool scan for ships
cli.alert.raise_(sound_alarm)     # mytool alert raise
```

### Reserved attribute names

A handful of names are instance attributes of the node class and cannot be
used as path tokens via attribute access: `name`, `description`, `symbol`,
`executable`, `hidden`. The most commonly encountered one is `name`.

Use a synonym (`named`, `nickname`, `callsign`, `id`) or subscript syntax as
a workaround:

```python
# This silently returns the string 'by', not a subcommand node:
cli.server.by._atomc_name[SERVER]  # ← WRONG

# Use a synonym instead:
cli.server.by.nickname[SERVER]  # ← OK
cli.server.named[SERVER]  # ← OK

# Or subscript syntax (less readable):
cli.server.by['name'][SERVER]  # ← OK
```

## Decorators

Decorating a function with a CLI node is equivalent to calling it with `call=`,
so argument auto-inference always applies:

```python
ENV     = Argument('env')
VERSION = Argument('version')

@cli.deploy[ENV].version[VERSION]
def do_deploy(env, version):    # parameter names matched to Argument names automatically
    ...

# Exactly equivalent to:
cli.deploy[ENV].version[VERSION](call=do_deploy)
```

That said, atomm-clap is designed for CLI definitions that read as a compact,
scannable list of command paths — the decorator form scatters that list across
the module alongside the handler implementations. A flat sequence of path
definitions is easier to audit and find entry points in.

Decorators can also introduce performance issues (see [below](#Performance)).


## Performance
One of the most useful features of atomm-clap are its automatic `--help` text generation and commandline completion.
Especially the latter is useful only if it is fast.  If, however, the cli script itself loads an entire application,
and especially if that leads to heavy libraries like `tensorflow` or similar to be loaded just to provide one
suggestion or print a help message, then that becomes a lot less fun.

It's therefore recommended to separate CLI definition, API definition, and application:

```python
## api.py
## Note: no imports on the top level.

def get_servers(cluster):
    """Prints the servers on the given cluster."""
    from my_application.kubernetes_app import get_servers as _get_servers
    servers = _get_servers(cluster)
    print(', '.join(servers))


def print_gpus():
    """Prints the GPUs that are currently available."""
    import tensorflow as tf # This can take half a second
    gpus = tf.config.list_physical_devices('GPU')
    if gpus:
        print('Have GPUs:')
        print(gpus)
        return True
    else:
        print("No GPUs found")
        return False
```

```python
## cli.py
import api

from atomc import CLI, parse_tokens, Argument

CLUSTER = Argument('cluster')

my_tool = CLI('my_tool')

my_tool.print.servers.for_[CLUSTER](api.get_servers, CLUSTER)
my_tool.print.gpus(api.get_gpus)
```

The following will return without loading anything from the actual application or from tensorflow:
```bash
$ my_tool print gpus --help
Prints the GPUs that are currently available.
```

Personally, we think separating concerns like this is a good pattern anyway.

## Printing and Logging
atomm-clap makes heavy use of Python's standard logging facility. 
In normal operation, it sets its own log level to `logging.ERROR` so its logs don't clutter the user application's 
output.
atomm-clap's log level can be set explicitly (`DEBUG`, `INFO`, ...) using the environment variable
`_ATOMC_OTHER_LOG_LEVELS`.

In commandline completion mode, atomm-clap sets the global log level to `logging.ERROR`, so application output doesn't
clutter the completion.  However, atomm-clap does not control `print()` statements, so if there are any `print()`
statements in the code that execute through imports, then the output of those `print()` statements will be treated as
completion items by the shell.

That should not happen, though, if the code is split into CLI definition, API definition, and application, as 
recommended under [Performance](#Performance), above. 
