Metadata-Version: 2.4
Name: lunaconf
Version: 0.6.0
Summary: Typed, layered configuration for Python evaluations and experiments
Author: Wenrui Huang
Author-email: Wenrui Huang <rijuyuezhuhwr@gmail.com>
License-Expression: MIT
License-File: LICENSE
Requires-Dist: pydantic>=2,<3
Requires-Dist: toml>=0.10.2,<1
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/rijuyuezhu/lunaconf
Project-URL: Repository, https://github.com/rijuyuezhu/lunaconf
Project-URL: Issues, https://github.com/rijuyuezhu/lunaconf/issues
Description-Content-Type: text/markdown

# lunaconf

`lunaconf` is a small configuration library for evaluations and experiments. It combines Pydantic models with ordered configuration layers from nested CLI assignments, JSON, TOML, environment-backed values, and argument files.

It supports Python 3.11 and newer.

## Installation

```bash
pip install lunaconf
```

## Define a configuration

```python
from typing import Self

from pydantic import Field

from lunaconf import LunaConf


class Config(LunaConf):
    required: int
    batch_size: int = 8
    model: str = "example/model"
    temperatures: list[float] = Field(default_factory=lambda: [0.0, 0.7])

    @classmethod
    def __lunaconf_default__(cls) -> Self:
        return cls(required=42)
```

`LunaConf` subclasses `pydantic.BaseModel`, so normal Pydantic validation, aliases, validators, nested models, and serialization options remain available. When aliases are defined, lunaconf uses the alias names in its mutable configuration dictionary and CLI paths.

Override `__lunaconf_default__` when the model contains required fields or needs application-specific defaults.

## Build from command-line layers

```python
from lunaconf import lunaconf_cli

config = lunaconf_cli(Config)
```

Nested fields and list indices use dot-separated paths. Numeric components address list indices; other non-empty components address dictionary keys, including names such as `model-id`:

```bash
python example.py batch_size=16 temperatures.1=0.5
python example.py 'model=another/model'
python example.py 'endpoint=https://example.test?a=1&b=2'
```

Values are parsed as JSON when possible. Otherwise they remain strings.

Multiple assignments can be separated with semicolons. A semicolon inside a quoted JSON string remains part of the value:

```bash
python example.py 'batch_size=16;model=another/model'
python example.py 'message="first; second";batch_size=16'
```

## Structured configuration sources

Sources are applied in command-line order, so later values override earlier values.

```bash
python example.py -J base.json -T machine.toml batch_size=32
```

Available sources:

- `-j JSON` / `-J FILE`: JSON object or JSON file.
- `-t TOML` / `-T FILE`: TOML document or TOML file.
- `-d DATA` / `-D FILE`: detect JSON first, then TOML.
- `-C FILE`: argument file containing commands and source options.
- positional `path=value`: nested assignment.

The top level of JSON and TOML input must be an object/table.

### List behavior

Structured configuration sources replace lists as a whole while dictionaries merge recursively:

```python
# default values: [1, 2, 3]
config = lunaconf_cli(Config, ["-j", '{"values": [9]}'])
# values == [9]
```

Use a positional index assignment when only one existing list element should change:

```bash
python example.py values.1=9
```

## Argument files

An argument file is parsed with shell-like quoting. Blank lines and comments are ignored.

```text
# config.args
-J "base config.json"
-T machine.toml
message="hello; world # literal"
batch_size=32 # trailing comment
```

Apply it with:

```bash
python example.py -C config.args
```


## Special values

Special values are case-insensitive:

- `<null>` becomes `None`.
- `<del>` deletes a dictionary field or list element.
- `<inf>`, `<-inf>`, and `<nan>` become non-finite floats.
- `<env:NAME>` reads an environment variable as a string.
- `<envint:NAME>` reads an environment variable and converts it to `int`.

Missing environment variables and invalid integer conversions raise `ValueError`.

When serializing, unsupported JSON/TOML values are represented using the same markers. Pydantic JSON mode is used so values such as `Path`, dates, URLs, and enums are converted to portable values first.

## Lower-level API

`lunaconf_gendict` applies the same ordered source logic to an existing dictionary and can extend an existing `argparse.ArgumentParser`:

```python
import argparse

from lunaconf import lunaconf_gendict

parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true")

raw = {"batch_size": 8}
namespace = lunaconf_gendict(raw, parser=parser)
```

## Printing resolved configuration

`lunaconf_cli` provides:

- `-p`: print JSON and exit.
- `-P`: print TOML and exit.
- `-a`: include fields whose values equal their defaults.
- `--json-indent N`: control JSON indentation.

## Development

The repository uses `uv`, Ruff, strict Pyright, pytest with branch coverage, pre-commit, and GitHub Actions.

```bash
uv sync --locked --group dev
uv run pre-commit install
just check
```

The CI matrix tests Python 3.11 and 3.12. Static analysis targets Python 3.11 syntax and behavior.

## Releases

Tags matching `v*` trigger `.github/workflows/release.yml`. The workflow:

1. verifies that the tag matches `project.version`;
2. runs formatting, linting, strict type checking, and tests;
3. removes old build artifacts and runs `uv build --no-sources`;
4. smoke-tests both the wheel and source distribution in isolated environments;
5. publishes with `uv publish` through PyPI Trusted Publishing.

No long-lived PyPI API token is required. The PyPI project must trust:

- owner: `rijuyuezhu`
- repository: `lunaconf`
- workflow: `release.yml`
- GitHub environment: `pypi`

The matching GitHub environment must also be created under **Settings → Environments → pypi**. Requiring manual approval for that environment is recommended.
