simpleArgParser 0.2.3
A simple typed argument parser using dataclasses and type hints. This project is largely generated by LLMs.
Requires Python: >=3.10

# SimpleArgParser

A simple typed argument parser for Python built on dataclasses. Define your config as a class, get CLI parsing, validation, and serialization.

This project is largely generated by LLMs.

## Installation

```bash
pip install -U simpleArgParser
uv add simpleArgParser
```

## Quick Start

```python
from dataclasses import dataclass
from simpleArgParser import parse_args

@dataclass
class Config:
    name: str           # required (no default)
    epochs: int = 10    # optional with default
    lr: float = 0.001

config = parse_args(Config)
```

```bash
python main.py --name experiment1 --epochs 20
```

## Features

### Required and Optional Arguments

Fields without defaults are required. Fields with defaults are optional.

```python
@dataclass
class Config:
    required_field: str                # must be provided
    optional_field: int = 42           # has a default
    optional_none: float | None = None # optional, defaults to None
```

```bash
python main.py --required_field hello
python main.py --required_field hello --optional_none 3.14
python main.py --required_field hello --optional_none none  # explicitly set to None
```

### Bool Arguments

Accepts `true/false`, `yes/no`, `t/f`, `y/n`, `1/0` (case-insensitive).

```python
@dataclass
class Config:
    verbose: bool = False
```

```bash
python main.py --verbose true
python main.py --verbose yes
```

### Enum Arguments

Pass in the enum member **name**. Choices are displayed in `--help`.

```python
import enum

class Mode(enum.Enum):
    train = "train"
    eval = "eval"

@dataclass
class Config:
    mode: Mode = Mode.train
```

```bash
python main.py --mode eval
```

### List Arguments

Comma-separated values. Supports `none` to pass `None`.

```python
@dataclass
class Config:
    devices: list[int] | None = None
```

```bash
python main.py --devices 0,1,2
python main.py --devices none
```

### Nested Dataclasses

Nest dataclasses as fields. Arguments use dot-separated names. Unique field names get short aliases automatically.

```python
@dataclass
class OptimizerConfig:
    lr: float = 0.001
    weight_decay: float = 0.01

@dataclass
class Config:
    name: str = "exp"
    optimizer: OptimizerConfig = field(default_factory=OptimizerConfig)
```

```bash
# full path always works
python main.py --optimizer.lr 0.01

# short alias works when the name is unique across all fields
python main.py --lr 0.01 --weight_decay 0.05
```

### Inheritance

Child dataclasses inherit parent fields. You can override defaults.

```python
@dataclass
class BaseConfig:
    seed: int = 42
    verbose: bool = False

@dataclass
class TrainConfig(BaseConfig):
    lr: float = 0.001
    verbose: bool = True  # override parent default
```

```bash
python main.py --seed 123 --lr 0.01
```

### Comments as Help Text

Comments above or inline with fields are extracted and shown in `--help`.

```python
@dataclass
class Config:
    # Learning rate for the optimizer
    lr: float = 0.001
    epochs: int = 10  # number of training epochs
```

```bash
python main.py --help
# shows:
#   --lr      (type: float) (default: 0.001) Learning rate for the optimizer
#   --epochs  (type: int) (default: 10) number of training epochs
```

### JSON Config Loading

Use `SpecialLoadMarker` to load defaults from a JSON file. Priority: command line > pass_in > JSON config > default values.

```python
from simpleArgParser import SpecialLoadMarker

@dataclass
class Config:
    lr: float = 0.001
    epochs: int = 10
    load_from: str | None = SpecialLoadMarker()
```

```json
{"lr": 0.01, "epochs": 50}
```

```bash
python main.py --load_from config.json            # uses JSON values
python main.py --load_from config.json --lr 0.1   # CLI overrides JSON
```

### Partial Defaults for Nested Dataclasses

When a nested dataclass has required fields, you can't use it directly as a default (e.g. `field(default_factory=SamplingConfig)` fails because `SamplingConfig` has required fields). Use `partial_defaults()` to provide defaults for **some** fields while keeping others required from CLI.

```python
from simpleArgParser import parse_args, partial_defaults

@dataclass
class InnerConfig:
    lr: float          # required
    weight_decay: float # required
    warmup: int = 100

@dataclass
class Config:
    # lr gets default 0.001, weight_decay stays required from CLI
    optimizer: InnerConfig = partial_defaults(lr=0.001)
```

```bash
python main.py --optimizer.weight_decay 0.01
# optimizer.lr = 0.001 (from partial_defaults)
# optimizer.weight_decay = 0.01 (from CLI)
# optimizer.warmup = 100 (field default)

python main.py --optimizer.weight_decay 0.01 --optimizer.lr 0.1
# CLI overrides partial_defaults
```

#### Nested partial_defaults

`partial_defaults` can be nested to set defaults at multiple levels:

```python
@dataclass
class LeafConfig:
    gen_n: int        # required
    cache: bool = True

@dataclass
class MiddleConfig:
    max_tokens: int   # required
    temperature: float = 0.6
    leaf: LeafConfig = partial_defaults(cache=False)

@dataclass
class OuterConfig:
    mid: MiddleConfig = partial_defaults(
        max_tokens=123,
        leaf=partial_defaults(cache=False),
    )
```

```bash
# Only gen_n is required — everything else has defaults
python main.py --mid.leaf.gen_n 10
# mid.max_tokens = 123
# mid.temperature = 0.6
# mid.leaf.gen_n = 10
# mid.leaf.cache = False

python main.py --mid.leaf.gen_n 10 --mid.leaf.cache true
# CLI overrides nested partial_defaults
```

In `--help`, fields with partial defaults show their default value, and fields without defaults show `(required)`:

```
--mid.leaf.gen_n   (type: int) (required)
--mid.max_tokens   (type: int) (default: 123)
--mid.leaf.cache   (type: bool) (default: False)
```

### Pre/Post Processing

Define `pre_process()` and `post_process()` methods for validation or side effects. They are called recursively on all nested dataclasses (pre_process top-down, post_process bottom-up).

```python
@dataclass
class Config:
    tp: int = 1

    def pre_process(self):
        print("validating...")

    def post_process(self):
        if self.tp < 1:
            raise ValueError("tp must be >= 1")
```

### Serialization

Convert configs to JSON or dict. Enum values are serialized by name.

```python
from simpleArgParser import to_json, to_dict

config = parse_args(Config)
print(to_json(config))   # JSON string
print(to_dict(config))   # Python dict
```

### Programmatic Usage

Use `pass_in` to provide arguments from code. Use `disable_cmd=True` to ignore `sys.argv`. Very useful for debugging and testing.

```python
config = parse_args(Config, pass_in=["--lr", "0.01", "--epochs", "5"])

# ignore command line entirely
config = parse_args(Config, pass_in=["--lr", "0.01"], disable_cmd=True)
```

### Subcommands (CLI Tools)

Build multi-command CLI tools with `parse_args_with_commands`. Define commands using enums for type-safe dispatching. Supports arbitrary nesting.

```python
import enum
from simpleArgParser import parse_args_with_commands

class Command(enum.Enum):
    train = "train"   # start training
    eval = "eval"     # run evaluation

@dataclass
class TrainConfig:
    lr: float = 0.001
    epochs: int = 10

@dataclass
class EvalConfig:
    checkpoint: str  # required

command, config = parse_args_with_commands(
    commands={
        Command.train: TrainConfig,
        Command.eval: EvalConfig,
    },
)

if command == (Command.train,):
    print(f"Training with lr={config.lr}")
elif command == (Command.eval,):
    print(f"Evaluating {config.checkpoint}")
```

```bash
mycli train --lr 0.01 --epochs 20
mycli eval --checkpoint best.pt
mycli --help
```

#### Nested Commands

Group commands into modules with nested dicts and separate enums per level. The returned `command` is a tuple of enum members.

```python
class Top(enum.Enum):
    model = "model"  # model operations
    data = "data"    # data operations

class ModelCmd(enum.Enum):
    train = "train"
    eval = "eval"

class DataCmd(enum.Enum):
    process = "process"

command, config = parse_args_with_commands(
    commands={
        Top.model: {
            ModelCmd.train: TrainConfig,
            ModelCmd.eval: EvalConfig,
        },
        Top.data: {
            DataCmd.process: ProcessConfig,
        },
    },
    description="My ML CLI",
)

# command == (Top.model, ModelCmd.train)
if command[0] == Top.model:
    if command[1] == ModelCmd.train:
        ...
```

```bash
mycli model train --lr 0.01
mycli data process --workers 8
mycli --help          # shows command tree
mycli model --help    # shows model sub-commands
```

#### Shared Config Across Commands

Embed a common config as a nested field. Short aliases are created automatically for unique field names.

```python
@dataclass
class CommonConfig:
    verbose: bool = False

@dataclass
class TrainConfig:
    lr: float = 0.001
    common: CommonConfig = field(default_factory=CommonConfig)
```

```bash
mycli train --verbose true          # alias for --common.verbose
mycli train --common.verbose true   # full path always works
```

## Help Output Ordering

Arguments in `--help` are sorted by: required first, then by nesting depth (shallow first), then alphabetically.