Metadata-Version: 2.4
Name: nonfig
Version: 0.1.2
Summary: Automatic Pydantic config generation from function signatures with hyperparameters
Author-email: "Sencer S." <nospam@gmail.com>
License: MIT
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pydantic>=2.0.0
Requires-Dist: annotated-types>=0.6.0
Provides-Extra: dev
Requires-Dist: basedpyright>=1.31.7; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: numpy>=2.3.5; extra == "dev"
Requires-Dist: pandas>=2.3.3; extra == "dev"
Requires-Dist: pandas-stubs>=2.0.0; extra == "dev"
Requires-Dist: pyyaml>=6.0.0; extra == "dev"
Requires-Dist: jaxtyping>=0.2.0; extra == "dev"
Requires-Dist: beartype>=0.15.0; extra == "dev"
Requires-Dist: jax>=0.4.0; extra == "dev"
Requires-Dist: jaxlib>=0.4.0; extra == "dev"
Requires-Dist: flax>=0.7.0; extra == "dev"
Requires-Dist: pre-commit>=4.1.0; extra == "dev"
Requires-Dist: mkdocs-material>=9.6.0; extra == "dev"
Requires-Dist: mkdocstrings[python]>=0.28.0; extra == "dev"
Provides-Extra: yaml
Requires-Dist: pyyaml>=6.0; extra == "yaml"
Dynamic: license-file

# nonfig

![CI](https://github.com/sencer/nonfig/actions/workflows/ci.yml/badge.svg)
[![codecov](https://codecov.io/gh/sencer/nonfig/branch/master/graph/badge.svg)](https://app.codecov.io/github/sencer/nonfig)

**Automatic Pydantic config generation from class and function signatures.**

`nonfig` turns any class or function into a configurable, serializable, and validated
component. By generating Pydantic models directly from your code's signatures, it
eliminates boilerplate while enforcing type safety and validation.

## Quick Example

```python
from nonfig import configurable, DEFAULT, Hyper, Ge


@configurable
class Optimizer:
  def __init__(self, lr: Hyper[float, Ge(0)] = 0.01, momentum: float = 0.9):
    self.lr = lr
    self.momentum = momentum


@configurable
class Model:
  def __init__(
    self,
    hidden_size: int = 128,
    optimizer: Optimizer = DEFAULT,  # Automatically transforms to Optimizer.Config
  ):
    self.hidden_size = hidden_size
    self.optimizer = optimizer


# Instantiate via Config
config = Model.Config(hidden_size=256, optimizer={"lr": 0.001})
model = config.make()  # Returns a Model instance with a real Optimizer
```

## Installation

```bash
pip install nonfig
# Optional YAML support:
pip install nonfig[yaml]
```

## Core Concepts

### Configuring Classes (Greedy)

When you apply `@configurable` to a class, `nonfig` captures **all** parameters from
your `__init__` method (or dataclass fields) and generates a `.Config` class.

```python
@configurable
class Model:
  def __init__(self, layers: int = 3, dropout: float = 0.1):
    self.layers = layers
    self.dropout = dropout


# Direct instantiation still works as usual
m = Model(layers=5)

# Config provides validation and serialization
config = Model.Config(layers=10)
m = config.make()  # Returns Model instance
```

### Configuring Functions (Selective)

For functions, `@configurable` is **selective**. It only extracts parameters marked
with `Hyper[T]`, `DEFAULT`, or a Config object. Other parameters are treated as
"runtime arguments" that must be passed when calling the configured function.

```python
from nonfig import configurable, Hyper, Gt


@configurable
def train(
  dataset,  # Runtime argument (not in Config)
  *,
  epochs: Hyper[int, Gt(0)] = 10,  # Hyperparameter
  lr: Hyper[float] = 0.01,  # Hyperparameter
):
  print(f"Training for {epochs} epochs with lr={lr}")


# Create a configured version of the function
trainer = train.Config(epochs=20).make()

# Call it with runtime arguments
trainer(my_dataset)  # Uses epochs=20, lr=0.01
```

### Nested Configuration & `DEFAULT`

The power of `nonfig` lies in its ability to compose configurations. Any
`@configurable` class used as a type hint is automatically transformed into a nested
configuration. Use `DEFAULT` to automatically instantiate the nested config with its
own defaults.

```python
@configurable
class Trainer:
  def __init__(self, optimizer: Optimizer = DEFAULT):
    self.optimizer = optimizer


# 'optimizer' can be passed as a dict, an Optimizer.Config, or an Optimizer instance
config = Trainer.Config(optimizer={"lr": 0.0001})
trainer = config.make()
assert isinstance(trainer.optimizer, Optimizer)
```

### Validation & Constraints

Use `Hyper[T, ...]` to attach Pydantic-style constraints to your parameters. These
are enforced whenever a `.Config` is created.

```python
from nonfig import configurable, Hyper, Ge, Le, Gt, MinLen, Pattern


@configurable
class Network:
  def __init__(
    self,
    lr: Hyper[float, Gt(0)] = 0.01,
    dropout: Hyper[float, Ge(0), Le(1)] = 0.5,
    name: Hyper[str, MinLen(3), Pattern(r"^[A-Z]")] = "Net",
  ): ...
```

**Available constraints:** `Ge` (>=), `Gt` (>), `Le` (<=), `Lt` (<), `MinLen`, `MaxLen`,
`MultipleOf`, `Pattern`.

---

## Features

### Inheritance & Smart Propagation

`nonfig` supports automatic inheritance. If a subclass of a `@configurable` class
includes `**kwargs` in its `__init__`, it automatically inherits all configurable
parameters from its parents.

```python
@configurable
class Base:
  def __init__(self, x: int = 1):
    self.x = x


class Sub(Base):
  def __init__(self, y: int = 2, **kwargs):
    super().__init__(**kwargs)
    self.y = y


# Sub.Config now has both 'x' and 'y'
config = Sub.Config(x=10, y=20)
```

### External Components (`wrap_external`)

Make third-party classes (e.g., from PyTorch or Scikit-learn) configurable without
modifying their code. `wrap_external` is always **greedy**, capturing every
parameter in the signature.

```python
from torch.optim import Adam
from nonfig import wrap_external

AdamConfig = wrap_external(Adam, overrides={"lr": Hyper[float, Gt(0)]})


@configurable
class Experiment:
  def __init__(self, opt: AdamConfig = DEFAULT):
    self.opt = opt
```

### Function Type Proxies (`.Type`)

When a `@configurable` function is used as a nested dependency, use `.Type` for
the type hint to enable proper configuration nesting and IDE support.

```python
@configurable
def preprocessor(data): ...


@configurable
class Pipeline:
  """A pipeline with a nested configurable preprocessor."""

  def __init__(self, prep: preprocessor.Type = DEFAULT):
    self.prep = prep


### Overriding Nested Defaults

You can override default parameters of nested configurations directly within the type annotation using the `Overrides` utility. This is particularly useful for setting different defaults for the same configurable component used in different contexts.

**Validation:** Since `nonfig` uses `extra="forbid"`, all overrides (including those in constant dictionaries) are strictly validated against the target's signature. Misspelled keys will raise a `ValidationError` at decoration time.

```python
from nonfig import Overrides


@configurable
class Strategy:
  def __init__(
    self,
    # Sets default window to 100 specifically for the long MA
    long_ma: Overrides[moving_average.Type, "window": 100] = DEFAULT,
    # Sets default window to 3 specifically for the short MA
    short_ma: Overrides[moving_average.Type, "window": 3] = DEFAULT,
  ):
    self.long_ma = long_ma
    self.short_ma = short_ma


You can also provide a full dictionary of overrides directly:

```python
D = {"window": 100}


@configurable
class Strategy:
  # Injects the entire dictionary D as overrides for the moving average component
  long_ma: Overrides[moving_average.Type, D] = DEFAULT
```

`Overrides` also supports multiple arguments, merging dictionaries, slice syntax, and even nested dictionaries to reach deeper into the configuration tree:

```python
@configurable
class ComplexStrategy:
  def __init__(
    self,
    # Merges dictionary, slice syntax, and nested dictionary
    ma: Overrides[
      nested_ma.Type, {"other": 10}, "window":50, "sub_component" : {"param": 1.0}
    ] = DEFAULT,
  ):
    self.ma = ma
```

### Leaf Markers

Use `Leaf[T]` to disable "Config magic" for a specific parameter. This forces the
field to accept **only** raw instances of the class, preventing `nonfig` from
transforming it into a nested configuration.

```python
from nonfig import Leaf


@configurable
class Processor:
  def __init__(self, model: Leaf[MyModel]):
    # 'model' must be a MyModel instance.
    # Passing a dict or MyModel.Config will raise a ValidationError.
    self.model = model
```

**Leaf vs. DEFAULT:**
- **`DEFAULT`** is for **Composition**: It says "This is a sub-component; please
  auto-configure it using its own defaults."
- **`Leaf[T]`** is for **Inversion of Control**: It says "This is an external
  dependency that I (the user) will provide as a pre-instantiated object."

You should **not** use `Leaf[T] = DEFAULT`. They represent opposite intents: `Leaf`
blocks the configuration transformation that `DEFAULT` specifically requests. This
is now enforced at runtime with a `TypeError`.

### JAX, Flax & jaxtyping

`nonfig` is designed for machine learning research:
- **`jaxtyping`**: Preserves dimension metadata and works with `@jaxtyped`.
- **Flax**: Supports `nn.Module` classes natively.
- **Order**: Always place `@configurable` as the **outermost** decorator.

---

## Tooling & CLI

### CLI Overrides (`run_cli`)

Easily run your experiments with command-line overrides using dot-notation.

```python
from nonfig import run_cli

if __name__ == "__main__":
  # python train.py model.layers=5 optimizer.lr=0.001
  result = run_cli(train)
```

### IDE Support (`nonfig-stubgen`)

Because `nonfig` generates classes dynamically, IDEs might need help with
autocompletion. Generate `.pyi` stubs for your entire project:

```bash
nonfig-stubgen src/
```

### Config Loaders

Support for JSON, TOML, and YAML (requires `pyyaml`):

```python
from nonfig import load_yaml

config_data = load_yaml("config.yaml")
config = Model.Config(**config_data)
```

---

## Performance

`nonfig` is optimized for high-performance ML loops. `Config.make()` uses a cached
factory that is 70-80% faster than a standard Pydantic `model_validate`.

| Pattern | Latency | Note |
| :--- | :--- | :--- |
| Raw `Class()` | ~0.15µs | Baseline |
| `Config.make()` (Class) | ~0.29µs | Reused config instance |
| `Config(...).make()` | ~1.85µs | Full lifecycle |

**Best Practice:** Call `.make()` once outside your training loop.

---

## Comparison

| Feature | nonfig | gin-config | hydra | tyro |
| :--- | :--- | :--- | :--- | :--- |
| **Philosophy** | Code-first | DI | YAML-first | CLI-first |
| **Validation** | Pydantic | None | Runtime | Parse-time |
| **Nesting** | Automatic | Manual | Manual | Automatic |
| **IDE Support** | Stubs (.pyi) | None | Partial | Full |

---

## License

MIT License
