Metadata-Version: 2.4
Name: optfunc2
Version: 0.2.8
Summary: Turn any Python function into a CLI command with zero boilerplate. Just add @cmdline and your function signature becomes the interface.
License: PyPA
License-File: LICENSE.txt
Author: bajeer
Author-email: z-bajeer@yeah.net
Requires-Python: >=3.8,<4.0
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
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
Requires-Dist: docstring-parser (>=0.16,<0.17)
Requires-Dist: prettytable (==3.11.0)
Project-URL: Bug Tracker, https://github.com/yjloong/optfunc2/issues
Project-URL: Homepage, https://github.com/yjloong/optfunc2
Description-Content-Type: text/markdown

<p align="center">
  <strong>optfunc2</strong><br>
  Auto-generate CLI from Python functions — zero boilerplate.
</p>

<p align="center">
  <a href="https://pypi.org/project/optfunc2/"><img src="https://img.shields.io/pypi/v/optfunc2?color=blue" alt="PyPI"></a>
  <a href="https://pypi.org/project/optfunc2/"><img src="https://img.shields.io/pypi/pyversions/optfunc2" alt="Python"></a>
  <a href="https://github.com/yjloong/optfunc2/blob/main/LICENSE.txt"><img src="https://img.shields.io/badge/license-PyPA-green" alt="License"></a>
</p>

---

> Turn any Python function into a CLI command — no `argparse`, no `click`, no boilerplate. Just add a decorator and your function's signature becomes the interface.

## Why optfunc2?

|  | **optfunc2** | argparse | click | typer |
|---|---|---|---|---|
| Lines to add a CLI command | **~1** (decorator) | ~15 | ~8 | ~5 |
| Auto-generated help from docstring | ✅ | ❌ | ❌ | ✅ |
| Type coercion from annotations | ✅ | Manual | Manual | ✅ |
| Shell abbreviation (`-a` for `--arg`) | ✅ | ❌ | ❌ | ❌ |
| Hex input support (`0x2A`) | ✅ | ❌ | ❌ | ❌ |
| Dependencies | **1** (`docstring-parser`) | stdlib | click | typer+click |
| External dependencies | prettytable (optional) | 0 | 7+ | 20+ |
| Union types (`int \| float`) | ✅ | ❌ | ❌ | ✅ |

## Install

```bash
pip install optfunc2
```

## Quick Start

```python
from optfunc2 import cmdline, cmdline_default, cmdline_start

@cmdline_default
def add(a: float, b: float):
    """Add two numbers

    Args:
        a: The first number
        b: The second number
    """
    print(f"{a} + {b} = {a + b}")

@cmdline
def multiply(x: int | float, y: int = 5):
    """Multiply two numbers

    Args:
        x: The first number
        y: The second number (default 5)
    """
    print(f"{x} × {y} = {x * y}")

if __name__ == "__main__":
    cmdline_start(header_doc="✨ calc CLI", has_abbrev=True)
```

```bash
$ python calc.py add --a 2.3 --b 3        # 2.3 + 3.0 = 5.3
$ python calc.py add -a 2.3 -b 3           # abbreviation works too
$ python calc.py multiply --x 3            # 3 × 5 = 15
$ python calc.py                           # uses default command
$ python calc.py help                      # show all commands
$ python calc.py add -h                    # show command help
```

## Features

### Decorators

| Decorator | Description |
|---|---|
| `@cmdline` | Register a function as a CLI command |
| `@cmdline_default` | Same as above, but also the default when no command is given |

### Type Support

| Type | Input Example | Notes |
|---|---|---|
| `int` | `--n 42` or `--n 0x2A` | Hex format supported |
| `float` | `--r 3.14` | |
| `str` | `--name hello` | |
| `bool` | `--verbose` (no value needed) | |
| `list` | `--items '[1, 2, 3]'` | Parsed via `ast.literal_eval` |
| `dict` | `--cfg '{"key": "val"}'` | Parsed via `ast.literal_eval` |
| `int \| float` | `--x 3` or `--x 2.5` | Union types supported |

### Argument Styles

```bash
# All of these are equivalent:
python app.py my_cmd --name hello --count 3
python app.py my_cmd --name=hello --count=3
python app.py my_cmd -n hello -c 3        # abbreviations (when has_abbrev=True)
python app.py my_cmd -nhello -c3          # abbreviation + value combined
```

### `cmdline_start()` Options

```python
cmdline_start(
    header_doc="My App",      # Header text shown in help
    has_abbrev=True,          # Enable single-char abbreviation (-a for --arg)
    print_retval=False,       # Print return value to stdout
)
```

### `called_directly()`

Check if the current function was invoked by optfunc2 (vs. called by another function):

```python
@cmdline
def main():
    if called_directly():
        print("Called from CLI")
    else:
        print("Called from another function")
```

## Help Output

```bash
$ python calc.py help
Usage: calc.py [command] [<args>|--help]

✨ calc CLI

commands:
    add          [default] Add two numbers
    multiply     Multiply two numbers

$ python calc.py add -h
Usage: calc.py add [OPTIONS]

Add two numbers

Arguments:
+------+--------+-------+---------+------------------+
| Opt  | Abbrev |  Type | Default |      Desc       |
+------+--------+-------+---------+------------------+
| --a  |   -a   | float |         | The first number |
| --b  |   -b   | float |         | The second number|
+------+--------+-------+---------+------------------+
```

## Real-World Example

```python
from optfunc2 import cmdline, cmdline_default, cmdline_start
import os

@cmdline_default
def list_files(directory: str = ".", show_size: bool = False):
    """List files in a directory

    Args:
        directory: Target directory (default ".")
        show_size: Show file size in bytes
    """
    for f in os.listdir(directory):
        path = os.path.join(directory, f)
        if show_size and os.path.isfile(path):
            print(f"{f} ({os.path.getsize(path)} bytes)")
        else:
            print(f)

if __name__ == "__main__":
    cmdline_start(header_doc="📁 file manager", has_abbrev=True)
```

## Limitations

- Variadic arguments (`*args`, `**kwargs`) are not supported
- Abbreviation conflicts (e.g., `text` and `test` both want `-t`) are resolved silently — first one wins
- Negative numbers as values require `--arg=-1` syntax (not `--arg -1`)

## License

PyPA License — see [LICENSE.txt](LICENSE.txt)

