vidhi 0.0.4
Configuration management system for complex systems
License: Apache-2.0
Requires Python: >=3.12
Keywords: config, configuration, dataclass, cli, yaml, polymorphic, immutable, frozen

# Vidhi

**Vidhi** (विधि, "method" in Sanskrit) is a Python configuration library for building type-safe, immutable configs with CLI and YAML support.

## Features

- **Immutable configs** - Frozen dataclasses prevent accidental modifications
- **CLI generation** - Auto-generate `--help` and argument parsing from config classes
- **Subcommands** - Build multi-command CLIs with `BaseCommand` (names, aliases, defaults)
- **Shortcuts** - Single-dash flags (`-r`, `-ld`) that map to Enum values
- **Positional args** - Accept values without `--flag` syntax
- **Polymorphic configs** - Runtime variant selection with type-safe inheritance
- **Nested configs** - Compose complex configurations from smaller pieces
- **Container fields** - `List`, `Dict`, and `Mapping` fields with bracket-access CLI syntax
- **YAML loading** - Load configs from files with `--config config.yaml`
- **IDE autocomplete** - Export JSON Schema for YAML file completion
- **Shell completion** - Tab completion for bash, zsh, and fish

## Installation

```bash
pip install vidhi
```

## Quick Start

### Basic Config

```python
from vidhi import frozen_dataclass, field, parse_cli_args

@frozen_dataclass
class TrainingConfig:
    learning_rate: float = field(0.001, help="Learning rate", name="lr")
    batch_size: int = field(32, help="Batch size")
    epochs: int = field(10, help="Number of epochs")

config = parse_cli_args(TrainingConfig)
```

```bash
python train.py --help
python train.py --lr 0.01 --batch_size 64
python train.py --config config.yaml
```

### Subcommands (BaseCommand)

Build multi-command CLIs where the first argument selects the command:

```python
from vidhi import BaseCommand, field, frozen_dataclass, parse_cli_args

@frozen_dataclass
class MyTool(BaseCommand):
    """My Tool — a multi-command CLI"""
    verbose: bool = field(False, help="Verbose output")

@frozen_dataclass
class Run(MyTool, name="run", alias="r", default=True):
    """Run a task"""
    target: str = field(".", positional=True)

@frozen_dataclass
class List(MyTool, name="list", alias="l"):
    """List available tasks"""

cmd = parse_cli_args(MyTool)
```

```bash
mytool run ./src         # explicit command + positional arg
mytool r ./src           # alias
mytool --verbose         # default command (Run) with base class flag
mytool list              # different command
mytool --help            # shows all commands with descriptions
mytool run --help        # shows Run-specific flags
```

Key concepts:
- **`name=`** registers the subcommand name (first positional arg)
- **`alias=`** registers a short alias (e.g., `"r"` for `"run"`)
- **`default=True`** makes this the command used when none is specified
- Base class fields (like `verbose`) are shared across all subcommands
- Each subcommand gets its own `--help` showing only its flags
- Registries are per-base-class, so multiple tools don't conflict

### Positional Args

Fields marked `positional=True` accept values without a `--flag` prefix:

```python
@frozen_dataclass
class Grep(BaseCommand):
    """Search tool"""

@frozen_dataclass
class Search(Grep, name="search", alias="s", default=True):
    """Search for a pattern"""
    pattern: str = field(None, positional=True)
    ignore_case: bool = field(False, help="Case insensitive", name="i")
```

```bash
grep search mypattern           # positional arg
grep s mypattern -i             # alias + positional + shortcut
grep mypattern --ignore_case    # default command + positional + flag
```

Rules:
- Only one field per config can be `positional=True`
- Positional args are optional (use `None` default) or required (no default)
- Positional args work with both `BaseCommand` subcommands and plain configs

### Shortcuts

Map Enum values to single-dash flags for concise CLIs:

```python
from enum import Enum

class Action(Enum):
    RUN = "run"
    LIST = "list"
    LIST_DIR = "list-dir"

@frozen_dataclass
class Config:
    action: Action = field(
        Action.RUN,
        shortcuts={Action.RUN: "r", Action.LIST: "l", Action.LIST_DIR: "ld"},
    )
    target: str = field(".", positional=True)
```

```bash
myapp -r             # sets action=Action.RUN
myapp -l             # sets action=Action.LIST
myapp -ld            # multi-char shortcut, sets action=Action.LIST_DIR
myapp -r ./src       # shortcut + positional arg
```

Rules:
- Keys must be Enum members, values are the flag strings (without `-`)
- Both single-char (`"r"`) and multi-char (`"ld"`) flags are supported
- `-h` is reserved for `--help` and cannot be used as a shortcut
- Shortcuts can be combined with regular `--flags` and positional args

### Nested Configs

```python
@frozen_dataclass
class DatabaseConfig:
    host: str = "localhost"
    port: int = 5432

@frozen_dataclass
class AppConfig:
    name: str = "app"
    database: DatabaseConfig = field(default_factory=DatabaseConfig)

config = parse_cli_args(AppConfig)
```

```bash
python app.py --database.host db.example.com --database.port 3306
```

### Container Fields (List, Dict, Mapping)

Use bracket-access syntax to set elements of `List`, `Dict`, and `Mapping` fields from the CLI:

```python
from dataclasses import field as dc_field
from typing import Dict, List, Mapping

@frozen_dataclass
class GPULocation:
    node_ip: str = "127.0.0.1"
    device_id: int = 0

@frozen_dataclass
class ResourceAllocation:
    gpus: List[GPULocation] = dc_field(default_factory=list)

@frozen_dataclass
class ServerConfig:
    name: str = "server"
    gpu_pools: Dict[str, List[GPULocation]] = dc_field(default_factory=dict)
    resource_mapping: Mapping[int, ResourceAllocation] = dc_field(default_factory=dict)

config = parse_cli_args(ServerConfig)
```

**List access** — index with `[N]`:
```bash
python server.py --resource_mapping[0].gpus[0].node_ip 10.0.0.1 \
                  --resource_mapping[0].gpus[0].device_id 0 \
                  --resource_mapping[0].gpus[1].node_ip 10.0.0.2
```

**Dict access** — index with `[key]`:
```bash
python server.py --gpu_pools[pool_a][0].node_ip 10.0.0.1 \
                  --gpu_pools[pool_a][0].device_id 0 \
                  --gpu_pools[pool_b][0].node_ip 10.0.0.3
```

**Mapping access** — index with `[key]` (key type is auto-coerced):
```bash
python server.py --resource_mapping[0].gpus[0].node_ip 10.0.0.1 \
                  --resource_mapping[1].gpus[0].node_ip 10.0.0.2
```

Container fields also work in YAML files:
```yaml
# config.yaml
gpu_pools:
  pool_a:
    - node_ip: "10.0.0.1"
      device_id: 0
  pool_b:
    - node_ip: "10.0.0.3"
      device_id: 1
resource_mapping:
  0:
    gpus:
      - node_ip: "10.0.0.1"
        device_id: 0
```

```bash
python server.py --config config.yaml
# CLI overrides work with container fields too:
python server.py --config config.yaml --resource_mapping[0].gpus[0].device_id 4
```

> **zsh users**: Quote bracket args with single quotes to avoid glob expansion:
> `'--resource_mapping[0].gpus[0].node_ip' 10.0.0.1`

### Polymorphic Configs

```python
from enum import Enum
from vidhi import BasePolyConfig, frozen_dataclass, field

class CacheType(Enum):
    MEMORY = "memory"
    REDIS = "redis"

@frozen_dataclass
class BaseCacheConfig(BasePolyConfig):
    ttl: int = 3600

    @classmethod
    def get_type(cls) -> CacheType:
        raise NotImplementedError()

@frozen_dataclass
class MemoryCacheConfig(BaseCacheConfig):
    max_size: int = 1000

    @classmethod
    def get_type(cls) -> CacheType:
        return CacheType.MEMORY

@frozen_dataclass
class RedisCacheConfig(BaseCacheConfig):
    host: str = "localhost"
    port: int = 6379

    @classmethod
    def get_type(cls) -> CacheType:
        return CacheType.REDIS

@frozen_dataclass
class AppConfig:
    cache: BaseCacheConfig = field(default_factory=MemoryCacheConfig)

config = parse_cli_args(AppConfig)
```

```bash
python app.py --cache_type redis --cache.host redis.example.com
python app.py --cache_type memory --cache.max_size 5000
```

### YAML Loading

```yaml
# config.yaml
cache_type: redis
cache:
  host: redis.example.com
  port: 6379
  ttl: 7200
```

```bash
python app.py --config config.yaml
python app.py --config config.yaml --cache.ttl 3600  # CLI overrides YAML
```

## CLI Features

Every Vidhi config automatically supports:

| Flag | Description |
|------|-------------|
| `--help` | Show help with all options organized by variant |
| `--config FILE` | Load configuration from YAML file |
| `--export-json-schema [FILE]` | Export JSON Schema for IDE autocomplete |
| `--install-shell-completions [SHELL]` | Install tab completion (bash/zsh/fish) |

## API Summary

| Function | Description |
|----------|-------------|
| `@frozen_dataclass` | Decorator for immutable config classes |
| `field(default, help=, name=, positional=, shortcuts=)` | Field with CLI metadata |
| `parse_cli_args(cls)` | Parse CLI into config (supports both flat configs and BaseCommand) |
| `with_cli_overrides(config)` | Override existing config from CLI |
| `load_yaml_config(path)` | Load YAML to dict |
| `create_class_from_dict(cls, dict)` | Create config from dict |
| `dataclass_to_dict(config)` | Serialize config to dict |
| `BasePolyConfig` | Base class for polymorphic configs |
| `BaseCommand` | Base class for subcommand CLIs (name, alias, default) |

## Documentation

Full documentation: https://project-vajra.github.io/vidhi

See [`examples/`](examples/) for runnable code samples.

## License

Apache License 2.0