Metadata-Version: 2.4
Name: paramattrs
Version: 0.1.0
Summary: Typed, validated, and observable parameter attributes for Python.
Author-email: Sami Laine <sami.jy.laine@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/sami-laine/paramattrs
Project-URL: Source, https://github.com/sami-laine/paramattrs
Keywords: parameters,configuration,data object
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: mypy>=2.1.0; extra == "dev"
Dynamic: license-file

# paramattrs

_Declarative, type‑safe parameters with metadata, validation, and conversion built in._

`paramattrs` is a small Python library for declaring typed class attributes that behave like configurable
parameters. It is useful when you want class-level metadata, runtime validation, type conversion, and
per-instance values without writing a lot of boilerplate.

The library is useful for:

- defining typed attributes on classes
- validating values before they are accepted
- converting incoming values into a canonical form
- documenting parameter intent with metadata such as descriptions and options
- inspecting declared parameters programmatically
- reacting to value changes in a simple way

## Why this library exists

This library is meant for lightweight, declarative parameter handling in Python applications. It is especially
useful when you want objects to expose configurable fields that are:

- strongly typed
- validated
- easy to inspect and introspect
- safe to use from scripts, examples, configuration loaders, or small UI/model layers

It is not intended to replace a full configuration framework, form system, or data-validation library.
Instead, it gives you a compact way to define and manage parameters directly on your classes.

## Installation

From Python Package Index (PyPI):

```bash
pip install paramattrs
```

From the project root:

```bash
pip install -e .
```

## Python compatibility

This library targets Python 3.12+ but includes a small compatibility
fallback: when running on Python versions older than 3.13 the package
will import `TypeVar`/`Generic` from `typing_extensions` to support
PEP 696 default type parameter behavior. For normal use on Python
3.13+ no additional dependencies are required.

## Quick start

```python
from paramattrs import Parameter
from paramattrs import Metadata


class App:
    host = Parameter[str](
        "localhost",
        meta=Metadata("Host", "Host name")
    )

    port = Parameter[int](
        8080,
        meta=Metadata("Port", "Port number")
    )


app = App()
app.host = "127.0.0.1"
app.port = 8080

print(app.host)
print(app.port)
```

Each `Parameter` is a descriptor, so it behaves like a normal class attribute while still storing values per instance.

## Core concepts

### 1. Typed parameters

You define a parameter with a default value and an optional type annotation:

```python
from paramattrs import Parameter


class Example:
    count = Parameter[int](0)
    name = Parameter[str]("demo")
```

The library checks that assigned values match the declared type whenever it can.

### 2. Validation

You can provide a validator to reject invalid values:

```python
from paramattrs import Parameter


class Example:
    value = Parameter[int](
        42,
        validator=lambda value: 0 <= value <= 100
    )
```

Validators may return `True` or `False`, or raise `ValueError` for more explicit failures.

You can also supply a validator class.

The Parameter will instantiate it and pass itself to the validator:

```python
class IsBetweenMinAndMax:
    def __init__(self, parameter):
        self.parameter = parameter

    def __call__(self, value):
        meta = self.parameter.meta
        if meta.min is not None and value < meta.min:
            raise ValueError("Value too small")
        if meta.max is not None and value > meta.max:
            raise ValueError("Value too large")
        return True
```

### 3. Conversion

Converters let you normalize values before they are stored:

```python
from paramattrs import Parameter


class Example:
    radius = Parameter[float](0.0, converter=float)
```

If you assign a string, it will be converted to a float before validation and storage.

### 4. Listeners and change notifications

You can react to value changes with either a class-level callback or instance-specific listeners.

```python
from paramattrs import Parameter


def on_change(instance, parameter_name, value):
    print(f"{parameter_name} changed to {value}")


class Example:
    a = Parameter[int](0, on_change=on_change)


example = Example()
example.a = 5
```

Listeners can also be registered for a specific instance:

```python
def listener(value):
    print("current value:", value)

parameter = get_parameter(example, "a")
parameter.connect(listener)

example.a = 6

parameter.disconnect(listener)
```

### 5. Metadata

Parameters can carry descriptive metadata through a dedicated Metadata object.

Metadata is intentionally kept separate from validation logic and parameter behavior. When a parameter is
assigned to a class, the library automatically populates its metadata with the parameter name, owner class,
inferred type, and current default value.

```python
from paramattrs import Metadata, Parameter

class Example:
    flag = Parameter[bool](
        True,
        meta=Metadata("Enabled", "Enable temperature control")
    )
```

Metadata is fully user‑extensible:

```python
from dataclasses import dataclass
from paramattrs import Metadata

@dataclass
class NumericMetadata(Metadata):
    min: float | None = None
    max: float | None = None
    unit: str | None = None
```

```python
class Example:
    gain = Parameter[float, NumericMetadata](
        19.0,
        meta=NumericMetadata(
            title="Gain",
            desciption="Output gain",
            min=20.0,
            max=26.0,
            unit="dB"
        )
    )
```

This keeps the API clean while allowing rich UI or documentation layers to introspect parameters easily.

### 6. Instance-specific values

Parameters are stored per instance by default, so different objects can have different values for the same
parameter:

```python
from paramattrs import Parameter


class Example:
    value = Parameter[int](1)


first = Example()
second = Example()

first.value = 10
print(first.value)   # 10
print(second.value)  # 1
```

## Inspecting parameters

The library provides helpers to inspect parameter declarations from classes or instances. When you pass
an instance to get_parameter() or get_parameters(), the returned parameter objects are bound to that instance
so you can inspect their current value and state.

```python
from paramattrs import Parameter, Metadata
from paramattrs import get_parameter, get_parameters

class Example:
    a = Parameter[int](10, meta=Metadata("A", "First parameter"))
    b = Parameter[str]("hello", meta=Metadata("B", "Second parameter"))


example = Example()
parameter_a = get_parameter(example, "a")
print(parameter_a.meta.title)

for parameter in get_parameters(example):
    print(parameter.name, parameter.meta.description)
```

You can also reset a bound parameter back to its default value:

```python
parameter_a.reset()
print(parameter_a.value)
```

## Reading and writing parameters (JSON)

The library plays nicely with external JSON configuration sources. The example below shows how to load
parameter values from a local JSON file or fetch them from a remote JSON endpoint, apply them to an instance,
then write the updated values back to disk for later use.

```python
import json
from paramattrs import Parameter


class App:
    host = Parameter[str]("localhost")
    port = Parameter[int](8080)
    debug = Parameter[bool](False)


def load_from_dict(instance, data: dict):
    for name, value in data.items():
        if hasattr(instance, name):
            setattr(instance, name, value)


app = App()

with open("config.json", "r", encoding="utf-8") as fh:
    load_from_dict(app, json.load(fh))
```

Fetch configuration from the cloud (HTTP JSON).

```python
import requests

resp = requests.get("https://example.com/app-config.json")
load_from_dict(app, resp.json())
```

Write back to disk.

```python
import json
from paramattrs import get_parameters

data = {p.name: p.value for p in get_parameters(app)}

with open("config.json", "w", encoding="utf-8") as fh:
    json.dump(data, fh, indent=2)
```

## Typical use cases

### Configuration objects

Use parameters to define application settings or configuration values that should be validated and easy to
inspect.

### Model or UI fields

Use parameters to define fields with descriptive metadata, allowed values, and runtime validation.

### Small declarative APIs

Use parameters when a class should expose a clear, discoverable set of configurable attributes without
manually implementing property setters.

### Parameter-driven plugins or components

Use parameters for plugin-like objects or components that expose configurable behavior.

## Example scripts

The repository contains several runnable examples that demonstrate the core features:

- [examples/basic.py](examples/basic.py) — basic parameter definition and inspection
- [examples/validating_values.py](examples/validating_values.py) — validation styles
- [examples/validation_with_validator_class.py](examples/validation_with_validator_class.py) — callable validator classes
- [examples/configuration.py](examples/configuration.py) — loading and applying configuration-like data
- [examples/listening_changes.py](examples/listening_changes.py) — change notifications
- [examples/subclassing.py](examples/subclassing.py) — custom parameter subclasses

## API overview

The public API is intentionally small:

- `Parameter` — the main descriptor type
- `get_parameter(obj_or_cls, name)` — retrieve a parameter declaration by name
- `get_parameters(obj_or_cls)` — list declared parameters for a class or instance
- `all_of(*validators)` — combine validators so all must pass
- `any_of(*validators)` — combine validators so at least one must pass

Other useful helpers and exceptions:

- `link_parameters(a, a_param, b, b_param)` — link two instance parameters so changes propagate
- `unlink_parameters(a, a_param, b, b_param)` — remove a previously installed link
- `Metadata` — the metadata dataclass used to describe parameters (title, description, owner, type, default)
- `ParameterNotFoundError` — raised by `get_parameter()` when a named parameter cannot be found
- `UnboundParameterError` — raised when attempting to access instance-only features on an unbound parameter

## Summary

`paramattrs` provides a simple way to define typed, validated, and inspectable class attributes.

It is a good fit when you want a lightweight parameter system for objects, configuration models, and small
frameworks without introducing heavy dependencies or complex abstractions.
