Skip to content

Latest commit

 

History

History
245 lines (195 loc) · 9.92 KB

File metadata and controls

245 lines (195 loc) · 9.92 KB

API Reference — codechu-config 0.1.0

Complete reference for every public symbol re-exported from the codechu_config package. Stdlib-only, Python 3.10+. TOML support uses the stdlib tomllib (read) and a stdlib emitter (write).

Symbol Kind Module
Field class codechu_config.field
Schema class codechu_config.schema
Config class codechu_config.config
Migration type alias codechu_config.config
ConfigError exception codechu_config._exceptions
ValidationError exception codechu_config._exceptions
MigrationError exception codechu_config._exceptions

Field

class Field:
    def __init__(
        self,
        type: type | None = None,
        default: Any = <unset>,
        choices: Iterable[Any] | None = None,
        range: tuple[float, float] | None = None,
        required: bool = False,
        doc: str = "",
    ) -> None: ...

    def has_default(self) -> bool: ...
    def validate(self, value: Any, *, key: str = "<field>") -> Any: ...

A single schema element. Validation order: coerce → type-check → range-check → choices-check.

Parameter Meaning
type Required type after coercion. Supported: str, int, float, bool, list, dict. None means "any type accepted".
default Used when the key is missing on load. If unset and required=False, the field defaults to None.
choices Allowed post-coercion values. ValidationError if no match.
range (low, high) inclusive numeric bound; only meaningful for int / float.
required If True and the key is absent in loaded data, ValidationError.
doc Human-readable description (surfaced by introspection tooling).
from codechu_config import Field
port = Field(type=int, default=8080, range=(1, 65535), doc="listening port")
mode = Field(type=str, default="dev", choices=("dev", "prod", "test"))
port.validate(8000)        # 8000
port.validate("8000")      # 8000 (coerced)
port.validate(99999)       # raises ValidationError

Field.validate(value, key=...) returns the post-coercion value and raises ValidationError on any failure — the key argument is used only to make error messages locate the offending entry.


Schema

class Schema:
    def __init__(self, fields: Mapping[str, Field]) -> None: ...
    def __contains__(self, key: str) -> bool: ...
    def __iter__(self) -> Iterator[str]: ...
    def __len__(self) -> int: ...
    def fields(self) -> dict[str, Field]: ...
    def field(self, key: str) -> Field | None: ...
    def defaults(self) -> dict[str, Any]: ...
    def validate(self, data: Mapping[str, Any]) -> dict[str, Any]: ...

A schema is a mapping of dotted keys → Field. Dotted keys ("net.port") are stored as nested dicts on disk and accessed flatly via Config.get / Config.set.

Method Behavior
defaults() Return a fresh nested dict containing every field's default.
validate(data) Validate data against every field. Missing required fields → ValidationError. Returns the cleaned dict.
field(key) Look up a Field by dotted key, or None.
fields() Return a copy of the underlying {key: Field} mapping.
from codechu_config import Field, Schema
schema = Schema({
    "net.port": Field(type=int, default=8080, range=(1, 65535)),
    "net.host": Field(type=str, default="0.0.0.0"),
    "mode":     Field(type=str, choices=("dev", "prod"), required=True),
})
schema.defaults()
# {"net": {"port": 8080, "host": "0.0.0.0"}, "mode": None}

Config

class Config:
    def __init__(
        self,
        schema: Schema,
        path: str | Path,
        *,
        format: str = "json",   # or "toml"
        migrations: Iterable[Migration] | None = None,
    ) -> None: ...

    # Introspection
    @property
    def path(self) -> Path: ...
    @property
    def format(self) -> str: ...
    @property
    def schema(self) -> Schema: ...
    def as_dict(self) -> dict[str, Any]: ...

    # I/O
    def load(self) -> Config: ...
    def save(self) -> Config: ...

    # Access (dotted keys)
    def get(self, key: str, default: Any = None) -> Any: ...
    def set(self, key: str, value: Any) -> None: ...
    def update(self, updates: Mapping[str, Any]) -> None: ...

    # dict-like sugar
    def __getitem__(self, key: str) -> Any: ...
    def __setitem__(self, key: str, value: Any) -> None: ...
    def __contains__(self, key: str) -> bool: ...

Runtime configuration object bound to a file. Constructed in memory with schema defaults — call .load() to read the file (or no-op if the file does not exist yet).

Constructor

Param Meaning
schema Validates every read and every write.
path File path. Parent directory must exist.
format "json" (default) or "toml". JSON is read+write; TOML is read+write via the stdlib emitter.
migrations Optional sequence of dict → dict functions, run in order when the on-disk _version is not current.

Methods

  • load() — read the file, run migrations if needed, validate. Returns self for chaining. If the file does not exist, in-memory state stays at schema defaults.
  • save() — re-validate, then write atomically (write-temp + os.replace). Returns self.
  • get(key, default=None) — dotted-key access. Falls back to the field's default, then to the supplied default.
  • set(key, value) — validates value against the field (if one exists) before storing.
  • update(mapping) — bulk update. Atomic: every value is validated on a candidate copy first; nothing is applied unless all keys pass.

Dict sugar

cfg["net.port"] is cfg.get("net.port"); cfg["net.port"] = 80 is cfg.set("net.port", 80); "net.port" in cfg matches either an explicit value or a defined schema field.

from codechu_config import Config
cfg = Config(schema, "~/app.json").load()
cfg["mode"] = "prod"
cfg.set("net.port", 9000)
cfg.save()

get() for an unknown key with no schema entry returns the supplied default (or None). __getitem__ raises KeyError in the same situation — mirror of dict.


Migration

Migration = Callable[[dict[str, Any]], dict[str, Any]]

A migration is a pure function dict → dict. Each migration is responsible for bumping _version itself, e.g.:

def v1_to_v2(d: dict) -> dict:
    if d.get("_version") != 1:
        return d
    d = {**d, "_version": 2}
    d["net"] = {"port": d.pop("port", 8080)}   # restructure
    return d

Migrations run in declared order and stop once _version equals the schema's _version default. Migrations are run on load(); the post-migration data is then validated against the schema.

Errors raised inside a migration are wrapped as MigrationError. Returning a non-dict is also a MigrationError.


Exceptions

ConfigError

Base class for every exception this library raises (other than the stdlib TypeError / KeyError used for programmer errors).

ValidationError

Raised by Field.validate, Schema.validate, Config.set, Config.update, and Config.load (after migrations).

MigrationError

Raised when a migration function fails or returns a non-dict.


Edge cases

  • Missing parent directory: save() does not create intermediate directories. Create them yourself before writing.
  • Atomic save: write goes through a sibling tempfile + atomic os.replace, so an interrupted save() leaves either the old file or the new one — never a half-written file.
  • TOML emitter: stdlib has no TOML writer in 3.10/3.11; this library ships a minimal one. Round-trips for str, int, float, bool, list, nested tables. Datetimes and arrays of tables are out of scope.
  • _version field: define it in your schema (typically with a default=N) when you want migrations to kick in. Without it, migrations are silently skipped.