Metadata-Version: 2.5
Name: confargs
Version: 0.3.0
Summary: Declarative CLI argument parser that merges command line, TOML config files, and environment variables.
Project-URL: Homepage, https://github.com/MarketSquare/confargs
Project-URL: Repository, https://github.com/MarketSquare/confargs
Project-URL: Issues, https://github.com/MarketSquare/confargs/issues
Author: confargs contributors
License-Expression: MIT
License-File: LICENSE
Keywords: argparse,arguments,cli,configuration,toml
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
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: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: tomli>=2.0; python_version < '3.11'
Description-Content-Type: text/markdown

# confargs

> ⚠️ Early development. APIs may change.

**confargs** is a small, declarative CLI argument parser for Python 3.10+ that
merges configuration from three sources into one result:

1. **Command line** arguments (`--log out.html`, `-l NONE`)
2. **Environment variables** (per-option or auto-generated)
3. **TOML config files** (discovered by walking up from the current directory,
   `pyproject.toml`-style)

You describe options as **methods** on a class. Each method receives the raw
value from whichever source supplied it, performs any parsing/validation you
like, and returns the final value. confargs handles discovery, precedence and
basic type coercion; your code owns the domain logic.

```python
import confargs
from confargs import ArgConfig


class MyArgs(ArgConfig):
    """My CLI tool.

    Longer description shown in --help.
    """

    name = "mytool"

    # Declarative option: no method needed when there's nothing to parse.
    title = confargs.option(name="title", default="report", help="Report title.")

    @confargs.option
    def log(self, value: str | None = "log.html") -> str | None:
        """HTML log file. Disable with the special value 'NONE'."""
        if value == "NONE":
            return None
        return value

    @confargs.option(name="console", short="c")
    def console(self, value: str = "verbose") -> str:
        choices = ["verbose", "dotted", "quiet", "none"]
        if value not in choices:
            raise confargs.OptionValueError(f"console must be one of {choices}")
        return value


config = confargs.ConfigurationProcessor(MyArgs).process()
print(config.title, config.log, config.console)
```

## Declaring options

Options come in two flavours:

- **Method-based** (`@confargs.option`): the decorated method receives the raw
  value and returns the parsed/validated result. Use this whenever you need to
  transform or validate the value.
- **Declarative** (`attr = confargs.option(name=..., help=...)`): a plain class
  attribute with no method, for simple values that need no custom handling. The
  value passes straight through coercion. Set `default=` (a `bool` makes it a
  flag, `None` makes it optional) and `type=` to control the value type.

## Precedence

Highest wins: **CLI > environment variables > nearest TOML > user-directory TOML > option default**.

## Configuration sources

### TOML files

Config is read from a table named after your tool. By default that is
`[tool.<name>]` (e.g. `[tool.mytool]`); override it with
`default_config_section = "tool.custom"`. The file names searched are set with
`config_names` (default `["pyproject.toml"]`). Both `dashed-keys` and
`snake_case_keys` are accepted.

```toml
[tool.mytool]
log = "results.html"
console = "dotted"
tags = ["ci", "nightly"]
```

**Discovery** walks up from the current directory looking for those files and
stops at the project root (a directory containing `.git`). If nothing is found,
a per-user config directory is consulted (`%APPDATA%\<name>` on Windows,
`$XDG_CONFIG_HOME/<name>` otherwise). Discovery is controlled by built-in,
CLI-only options:

- `--config PATH` — use only this file, skip discovery.
- `--no-config` — ignore config files entirely.
- `--ignore-git` — keep searching above the `.git` project root.

By default (`strict_config = True`) unknown keys — and any option declared with
`config=False` — found in the config section raise an error, which catches typos
early. Set `strict_config = False` on your class to silently ignore them instead.

### Environment variables

Reading from the environment is **opt-in per option**. Pass `env=True` to use a
generated name, or `env="MY_NAME"` for an explicit one:

```python
@option(env=True)  # reads $MYTOOL_LOG (from the class template)
def log(self, value: str = "log.html") -> str: ...


@option(env="LOG_FILE")  # reads $LOG_FILE
def log2(self, value: str = "log.html") -> str: ...
```

The generated name comes from the class `env_var_template` (default
`"{name}_{option}"`), formatted with the tool `name` and the `option` attribute
name and upper-cased — e.g. `MYTOOL_LOG`. Override it per class:

```python
class Args(ArgConfig):
    name = "mytool"
    env_var_template = "MYTOOL_CFG_{option}"  # -> MYTOOL_CFG_LOG
```

### Restricting where an option is read from

Two independent toggles control which sources feed an option:

- `@option(cli=False)` hides the option from the command line (no CLI names, not
  shown in `--help`) — use for options that should only come from config files
  or the environment.
- `@option(config=False)` stops the option being loaded from TOML config files —
  use for switches that control the tool run itself (the built-in discovery
  options above are defined this way).

Combine them as needed, e.g. a CLI-only switch is `@option(config=False)` with
`env` left off.

## Options in depth

- Long names come from the method name (`dry_run` → `--dry-run`); a short name
  is derived from the first letter when it is still free. Override either with
  `name="console"` and/or `short="c"` (passing `name` opts out of the implicit
  short — add `short=` to keep one).
- The value type is taken from the `value` parameter annotation. `bool` becomes
  a flag; `list[...]` becomes a repeatable option; `int`/`float`/`str` are
  coerced from strings. Your method receives the coerced value and returns the
  final one — raise `confargs.OptionValueError` to reject it.
- Boolean options can be negated on the command line: `--verbose` sets it to
  `True`, `--no-verbose` sets it to `False`.

### Eager options and argument files

Mark an option `is_eager=True` to resolve it *before* every other source,
directly against `argv`. The method's return value — an iterable of tokens or
`None` — replaces the option's own arguments, so it can inject more options.
This is how an `--argumentfile` option expands a file (Robot Framework style)
into extra arguments, including nested argument files:

```python
from confargs import ArgConfig, option, read_argument_file


class Args(ArgConfig):
    @option(name="argumentfile", short="A", config=False, is_eager=True)
    def argumentfile(self, value: str | None = None) -> list[str] | None:
        return read_argument_file(value) if value else None
```

`ConfigurationProcessor(Args, argv=[...])` accepts an explicit argument list;
when omitted it falls back to `sys.argv[1:]`.

## Example

A complete, self-contained example lives in [`examples/demo.py`](examples/demo.py)
(with a sample [`examples/example.args`](examples/example.args) and
[`examples/README.md`](examples/README.md)). It's a single copy-pasteable file
showing value options, `--no-` flag negation, environment variables and an
eager `--argumentfile`. Run it from a checkout without installing anything:

```bash
uv run python examples/demo.py --who Ada --repeat 3
uv run python examples/demo.py -A examples/example.args
uv run python examples/demo.py --help
```

Separately, the packaged [`confargs.demo`](src/confargs/demo.py) module is
installed as the `confargs-demo` console script via `[project.scripts]`:

```bash
uv run confargs-demo --console quiet --retries 5
uv run confargs-demo --help
```

To ship your own tool, point a console script at a `main()` that runs the
processor, for example in `pyproject.toml`:

```toml
[project.scripts]
mytool = "mytool.cli:main"
```

## Development

This project uses [uv](https://docs.astral.sh/uv/).

```bash
uv sync                 # create the environment
uv run pytest           # run the tests
uv run ruff check       # lint
uv run ruff format      # format
uv run mypy             # type-check
pre-commit install      # enable git hooks
```

## Publishing

Releases are published to PyPI by `.github/workflows/publish.yml` when a GitHub
Release is published. It uses [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
(OIDC), so no API token is stored in the repository — configure the project as a
trusted publisher on PyPI (workflow `publish.yml`, environment `pypi`) once.

## Versioning

confargs follows [Semantic Versioning](https://semver.org/). The version is
single-sourced from `__version__` in `src/confargs/__init__.py` (hatchling reads
it at build time). While the project is `0.x.y` the API is still stabilising, so
minor releases may include breaking changes. Notable changes are recorded in
[`CHANGELOG.md`](CHANGELOG.md).

Releases are automated with
[release-please](https://github.com/googleapis/release-please): merging
[Conventional Commits](https://www.conventionalcommits.org/) to `main` keeps an
open release PR that bumps `__version__`, updates the changelog and, once merged,
tags the release and publishes to PyPI. Pre-1.0, breaking changes bump the minor
version (`bump-minor-pre-major`).

## License

MIT
