Metadata-Version: 2.5
Name: eafig
Version: 2.1.0
Summary: Manage your hyperparameters more easily.
Project-URL: Homepage, https://github.com/MugeTong/eafig
Project-URL: Issues, https://github.com/MugeTong/eafig/issues
Author-email: MugeTong <here5320@gmail.com>
License: MIT License
        
        Copyright (c) 2026 MugeTong
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: omegaconf
Requires-Dist: pyyaml
Description-Content-Type: text/markdown

# Eafig

Manage your hyperparameters from the outside.

## Installation

```bash
pip install eafig
```

Requires Python ≥ 3.12.

## Quick Start

```python
import eafig
from eafig import configclass

@configclass("training")
class TrainingConfig:
    a: int = 1
    c: float = 1.0

@configclass(name="model")
class ModelConfig:
    x: str = "hello"
    y: str = "world"


# Load from file, then CLI — later calls win
eafig.load("config/default.yaml")
eafig.from_cli()

# Save to file
eafig.save("config/saved_config.yaml")

# Instantiate — values come from file/CLI or defaults
training = TrainingConfig()
model = ModelConfig()
```

## Core ideas

### Group-only layers

There is no "root config". Every config class is a named group registered with
`@configclass("path")`. The path is dot-separated for nesting:

```python
@configclass("model")
class ModelConfig:
    hidden_dim: int = 256

@configclass("model.optimizer")
class OptimizerConfig:
    lr: float = 1e-3
```

### Every field needs a default

Fields must declare a default value (or a `default_factory`). Registering a field
without one raises a `TypeError`:

```python
@configclass("model")
class ModelConfig:
    hidden_dim: int = 256   # OK
    tasks: list[str] = ["train", "evaluate"]  # mutable defaults are supported
    # lr: float            # TypeError: must provide a default value
```

`@configclass` automatically gives `list` and `dict` defaults an
independent `default_factory`. You can still use `dataclasses.field` explicitly
when a custom factory is needed.

## Config loading order

```
defaults  <  file (load)  <  CLI (from_cli)
```

Each layer overrides the one before it. Among `load()` / `from_cli()` calls,
later calls win.

### `keep_cli`

A later `load()` normally overrides CLI values. Pass `keep_cli=True` to keep CLI
on top:

```python
eafig.from_cli()
eafig.load("config.yaml", keep_cli=True)  # CLI stays above file
```

### `load_by_cli`

Load a config file whose path comes from a command-line flag:

```python
# python app.py --config config.yaml
eafig.load_by_cli("config", keep_cli=False)  # file wins conflicts (default)
# eafig.load_by_cli("config", keep_cli=True) # CLI wins conflicts
```

`load_by_cli()` always reads all CLI arguments. `keep_cli` only decides precedence
when a key appears both on the command line and in the selected file.

The flag is registered as the single root schema field, remains in the stored
configuration, and is included by `save()`. Because the root schema may only be
registered once, `load_by_cli()` may only be called once per process when the flag
is present; a second call raises `ValueError`.

## CLI overrides

```
--model.hidden_dim 1024 --model.optimizer.lr 1e-3
```

A flag with no value is `True`; dotted keys nest. An explicitly supplied empty
argument remains an empty string:

```python
eafig.from_cli(["--debug", "--name", ""])
assert eafig.get("debug") is True
assert eafig.get("name") == ""
```

## Unknown keys (`ignore_unknown_keys`)

By default a config group rejects keys that are not declared fields or child
groups. The rejection happens when the config is read — when you instantiate the
group or call `save()`:

```python
@configclass("model")
class ModelConfig:
    hidden_dim: int = 256

eafig.load("config.yaml")   # config.yaml has model.typo_key → loaded without error
model = ModelConfig()       # KeyError: Invalid key(s) {'typo_key'}
```

Set `ignore_unknown_keys=True` to tolerate undeclared keys. They are ignored rather
than added to the config class or treated as child configuration groups:

```python
@configclass("model", ignore_unknown_keys=True)
class ModelConfig:
    hidden_dim: int = 256
```

Schema registration may happen after `load()`. Loading performs structural
validation for schema paths already known at that time; unknown-key validation is
deferred until a group is instantiated or the configuration is saved.

## Hidden groups

```python
@configclass("api", hidden=True)
class ApiConfig:
    secret_key: str = "..."
```

Hidden groups are excluded from `save()` and from recursive reads by default.

## Frozen groups

```python
@configclass("model", frozen=True)
class ModelConfig:
    hidden_dim: int = 256

m = ModelConfig()          # OK — uses defaults
m.hidden_dim = 1024        # FrozenInstanceError
```

## Runtime get

```python
value = eafig.get("model.hidden_dim")        # 256
value = eafig.get("missing.key", default=0)  # 0
```

## API reference

| API | Description |
|-----|-------------|
| `@configclass(name, *, frozen=False, hidden=False, ignore_unknown_keys=False)` | Register a dataclass as a config group |
| `eafig.load(path=None, keep_cli=False)` | Load a YAML file or file-like object |
| `eafig.from_cli(args=None)` | Parse CLI args (default: `sys.argv[1:]`) |
| `eafig.load_by_cli(flag, keep_cli=False)` | Load a file path taken from a CLI flag |
| `eafig.save(path, sort_keys=True)` | Save the config to YAML |
| `eafig.config` | Access the current complete configuration as a dictionary |
| `eafig.get(key, default=None)` | Get a single value (dot notation) |

## Examples

See [examples/](examples/).

## License

MIT
