Metadata-Version: 2.4
Name: sigil-cli
Version: 1.5.2
Summary: Configuration driven CLI builder with subcommands and script loading
Author-email: Kenzo Staelens <kenzostael@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/kenzo-staelens/sigil
Project-URL: Repository, https://github.com/kenzo-staelens/sigil
Project-URL: Issues, https://github.com/kenzo-staelens/sigil/issues
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: User Interfaces
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml>=6.0
Provides-Extra: completion
Requires-Dist: argcomplete>=3.0; extra == "completion"
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Dynamic: license-file

# Sigil

> Declarative argparse, without the CLI boilerplate.

Sigil is a lightweight, declarative CLI framework for Python. Define your command tree in YAML (or any other format),
and sigil builds the `argparse` parser on the fly. Complete with subcommands and dynamic script loading.  
It plays nicely with `argcomplete` out of the box.

[![PyPI Version](https://img.shields.io/pypi/v/sigil-cli)](https://pypi.python.org/pypi/sigil-cli)
[![PyPI Wheel](https://img.shields.io/pypi/wheel/sigil-cli.svg)](https://pypi.org/project/sigil-cli/)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/release/python-3100/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Downloads](https://static.pepy.tech/badge/sigil-cli)](https://pepy.tech/project/sigil-cli)
[![Tests](https://github.com/kenzo-staelens/sigil/actions/workflows/tests.yml/badge.svg)](https://github.com/kenzo-staelens/sigil/actions/workflows/tests.yml)
[![Coverage Status](https://coveralls.io/repos/github/kenzo-staelens/sigil/badge.svg?branch=main)](https://coveralls.io/github/kenzo-staelens/sigil?branch=main)

<div align="center">
  <sub>in production since April 2026</sub>
</div>

---

## Features

- Declarative command hierarchies (parents, subparsers, defaults)
- Each command can point to a dynamically imported Python script
- `argcomplete` integration for tab‑completion
- Pluggable data sources – YAML is the default, but JSON, TOML, or a dict are trivial to swap in
- No boilerplate argparse code in your main logic

## The alternatives

There are plenty of established options out there. [Click](https://click.palletsprojects.com/)
and [Typer](https://typer.tiangolo.com/) are great libraries with their own
approaches.

Sigil takes a different path, focusing on reducing boilerplate while keeping
your command structure modular and flexible.

## Quick Start

### Install

```bash
pip install sigil-cli
```

*optionally include argcomplete with `sigil-cli[completion]`*

The `init` command generates a project directory with a sample configuration and entrypoint:

```bash
sigil init demo
sigil validate demo/ # optional, should not output anything for correct configurations
cd demo
python main.py --help
```

For a full walkthrough with custom commands and arguments, jump to the Quick Start below.

### 0. Recommended file structure

```text
project_root/
├── mycli.py              # drop‑in bootstrap script (alias this)
├── manifest.yml          # lists all YAML config files to load
├── yml/                  
│   ├── root.yml          # root command definition
│   ├── root_run.yml      # subcommand definition(s)
│   └── ...               
└── scripts/              
    ├── run.py            # implements the 'run' command
    └── ...               # other scripts
```

ps: don't shoot yourself in the foot, don't symlink the bootstrap script.

### 1. Entry script

Create `mycli.py`:

```python
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
from pathlib import Path
from sigil import run_from_config

if __name__ == "__main__":
    run_from_config(Path(__file__).parent)
```

### 2. Configuration files

List all your YAML definitions in `manifest.yml`:

```yaml
- root.yml
- root_run.yml
```

Define the root command in `root.yml`:

```yaml
root:
  name: mycli
  script_dir: scripts
```

Define a subcommand in `root_run.yml`:

```yaml
root_run:
  name: run
  parent: root
  help: command utility to run containers
  script: run
  args:
    - help: port to run, autoincrements from 8080
      name:
        - -p
        - --port
```

### 3. Write the script

Create `scripts/run.py`:

```python
import argparse
from typing import Any

def run(args: argparse.Namespace, ctx: dict[str, Any]) -> None:
    port = getattr(args, "port", 8080)
    port = find_next_free_port_logic(port)
    print(f"Running container on port {port}")
```

### 4. Run it

```bash
chmod +x mycli.py
./mycli.py run --port 9000
# Running container on port 9000

./mycli.py run
# Running container on port 8080

./mycli.py run
# Running container on port 8081
```

## Configuration Reference

### Root Command

| Field | Description |
| --- | --- |
| `name` | Program name (used as `prog` in argparse) |
| `script_dir` | Directory (relative to the config root) where command scripts are located |

### Command

| Field | Description |
| --- | --- |
| `name` | Subcommand name |
| `parent` | Parent command (must exist elsewhere in a config) |
| `help` | Help text for this subcommand |
| `script` | Python module name (without `.py`) inside `script_dir`, absolute paths supported |
| `args` | List of argument definitions (see below) |
| `default` | If `True`, this subcommand is used when no subcommand is given |
| `load` | If `False` skips this command (or top level object) from being loaded into the command tree (default `True`) |
| any other parser kwarg | except for `dest`, `parents` and `formatter_class` they are all supported |

Note that `parent` does not refer to argparse's `parents` parameter but is only used to resolve the parser tree.
Parser (multi-)inheritance isn't supported but can be emulated by adding arguments to `parent` commands in the tree.

### Argument

Each argument entry can be a plain dict which maps 1-to-1 with argparse `add_argument`, except name which maps it's `*args`

```yaml
- name: ["-p", "--port"]   # or a single string, e.g. "positional"
  required: false
  default: 8069
  help: "port number"
```

Groups and mutex groups are also suppored via the "kind" parameter (defaults to `argument`)

```yaml
# mutex group
- kind: mutex
  args:
    - <any recursive args/group/mutex construct here>
    ...
  ... # any valid mutex group arguments go here

- kind: group
  ...  # same
```

The `name` field can be `--flag` for flags or a string for positional arguments.
Both literal string and list of strings are supported.

Types (`type:`) only supports python builtins

### Script files

Each script files have as only requirement that they need to define a
`def run(args: argparse.Namespace, ctx: dict[str, Any]) -> None` method.

args is the by argparse supplied namespace (parsed with parse_known_args), any additional args can be found in `ctx['other_args']`

scripts run in sequence from command -> subcommand -> sub sub command -> ... and each may add to,
remove or otherwise modify args.namespace and ctx to enrich or modify the behaviour of supsequent scripts.

## Sigil CLI Commands

`sigil` ships with its own lightweight toolbelt to manage your projects:

| Command | Purpose |
| --- | --- |
| `sigil init [project_name]` | Creates a new project folder with a sample ready-to-run Python entrypoint. |
| `sigil validate [project_path]` | Checks your sigil definition for schema errors and missing references. Run this after heavy edits to catch mistakes early. |
| `sigil tree [project_path]` | Print the command structure of a sigil. |

*(Note: Your generated CLI (the one you build with Sigil) is completely separate from the `sigil` management
commands above. You alias and run `main.py`. the `sigil` prefix is a different namespace.)*

### Misc

`load_ignore` may be used to detaching commands from the command tree for any purpose
(deprecation, development, etc) or for non schema-compliant objects at the top level of a file, this may be useful to
define anchors or references that should not directly be read as a command.

## Tab‑Completion (argcomplete)

Sigil registers itself with `argcomplete` automatically if available on your system.  
To enable completion, install [argcomplete](https://github.com/kislyuk/argcomplete) and activate it for your entry
script (or use the builtin argcomplete comment):

```bash
pip install argcomplete
activate-global-python-argcomplete
```

Then run your script and hit <kbd>Tab</kbd> – subcommands and flags will complete.

## Pluggable Backends

Sigil uses yaml by default, but you can supply any datasource that we can convert it's output into `ParserConfig`:

```python
from sigil import run_from_config

# Use JSON instead:
class JsonReader:
    @classmethod
    def read_manifest(cls, config_root: Path, target: str) -> list | None:
        # read target paths for loading configuration
        ...
    
    def read_configuration(cls, target: Path, target: str) -> dict | None:
        # read *.json, parse, convert to dict of raw data
        ...


run_from_config("/path/to/config", datasource=JsonReader)
```

You can also pass a pre‑loaded dictionary directly by wrapping it:

```python
run_from_config(my_dict, datasource=DictReader)
```
