Practical recipes

These programs complement the persistent examples in Examples. Run them from an installed checkout with python examples/<name>.py. All files created by the recipes below use automatically cleaned temporary directories; the interactive collection examples otherwise work entirely in memory.

For first-run setup, Apply/Cancel, resets, recent documents, window geometry, account preferences, session overrides, and preference transfer, see the ten additional walkthroughs in User preference scenarios.

A first store without persistent files

The result contains port 9000 while the original snapshot retains 8080. The exact temporary path differs each run.

"""Try storage in an isolated directory that is removed when the demo ends."""

from pathlib import Path
from tempfile import TemporaryDirectory

from upref import ConfigStore


def main() -> None:
    """Demonstrate defaults, nested updates, and detached snapshots."""
    with TemporaryDirectory(prefix="upref-portable-") as directory:
        store = ConfigStore("portable-demo", directory=Path(directory).resolve())
        settings = store.load(defaults={"theme": "dark", "network": {"port": 8080}})
        assert not store.exists()  # Loading defaults never writes a file.
        store.save(settings)
        store.update({"network": {"port": 9000}})
        print(store.load())
        print(f"Original snapshot: {settings}")
        print(f"Temporary configuration: {store.path}")


if __name__ == "__main__":
    main()

Boolean and optional input

Try maybe and then no: the first answer triggers a retry and the second produces False. Enter submits an empty optional note. Ctrl+C cancels collection.

"""Collect a boolean and an optional note without saving anything."""

from upref import Field, PromptCancelled, collect, parse_bool


def main() -> None:
    """Ask for two values, retry invalid input, and handle cancellation."""
    schema = {
        "notifications": Field("Enable notifications", parser=parse_bool),
        "note": Field("Optional note", required=False),
    }
    try:
        print(collect(schema))
    except PromptCancelled:
        print("Cancelled; no values saved.")


if __name__ == "__main__":
    main()

Editing current values in a terminal

Press Enter twice to retain the existing values. Supply [] to replace the list with an empty list. Custom formatters keep the displayed text compatible with the parser. Parser and validator failures are retried.

"""Edit current terminal values with Enter to keep and JSON list parsing."""

import json

from upref import Field, PromptCancelled, collect, parse_bool
from upref.tty import TTYPrompter


def main() -> None:
    """Edit an in-memory configuration using a caller-owned terminal interface."""
    schema = {
        "enabled": Field("Enabled", parser=parse_bool),
        "tags": Field(
            "Tags",
            description='Enter a JSON list, for example ["work", "personal"].',
            parser=json.loads,
            formatter=json.dumps,
            validator=lambda value: (
                isinstance(value, list) and all(isinstance(tag, str) for tag in value)
            ),
        ),
    }
    try:
        settings = collect(
            schema,
            initial={"enabled": False, "tags": ["work"]},
            interface=TTYPrompter(keep_current=True),
            mode="all",
        )
    except PromptCancelled:
        print("Editing cancelled.")
    else:
        print(settings)
        print("Values are in memory only; call store.save(settings) to persist them.")


if __name__ == "__main__":
    main()

Graphical collection

Install upref[gui] first. This example uses the context manager to close only the GUI resources it owns. It also reports an unavailable GUI without a traceback. Existing JSON values are prefilled in valid JSON syntax.

"""Edit values in wxPython dialogs; install upref[gui] before running."""

import json

from upref import Field, PromptCancelled, PromptUnavailableError, collect, parse_bool
from upref.gui import GuiPrompter


def main() -> None:
    """Use a custom title, formatted current values, and explicit GUI ownership."""
    schema = {
        "enabled": Field("Enable notifications", parser=parse_bool),
        "tags": Field(
            "Tags",
            parser=json.loads,
            formatter=json.dumps,
            description="A JSON list of labels.",
            validator=lambda value: isinstance(value, list),
        ),
    }
    try:
        with GuiPrompter(title="Upref example preferences") as prompter:
            settings = collect(
                schema,
                {"enabled": False, "tags": ["work", "home"]},
                interface=prompter,
                mode="all",
            )
    except PromptUnavailableError:
        print('GUI unavailable. Install "upref[gui]" and run in a desktop session.')
    except PromptCancelled:
        print("Cancelled; no values saved.")
    else:
        print(settings)
        print("No file was written.")


if __name__ == "__main__":
    main()

Editing a nested section

Schemas describe top-level keys, so collect a section and then put it back into the complete mapping. Saving happens only after every prompt succeeds; cancellation leaves the stored data untouched. The unrelated theme remains.

"""Collect a nested section and save only after the whole interaction succeeds."""

from pathlib import Path
from tempfile import TemporaryDirectory

from upref import ConfigStore, Field, PromptCancelled, collect
from upref.tty import TTYPrompter


def main() -> None:
    """Edit a network section while preserving unrelated preferences."""
    with TemporaryDirectory(prefix="upref-nested-") as directory:
        store = ConfigStore("nested-demo", directory=Path(directory).resolve())
        settings = store.load(
            defaults={"theme": "dark", "network": {"host": "localhost", "port": 8080}}
        )
        network = settings["network"]
        if not isinstance(network, dict):
            raise ValueError("network must be a mapping")
        try:
            settings["network"] = collect(
                {
                    "host": Field(
                        "Host",
                        parser=str.strip,
                        validator=lambda value: isinstance(value, str) and bool(value),
                    ),
                    "port": Field(
                        "Port",
                        parser=int,
                        validator=lambda value: (
                            type(value) is int and 1 <= value <= 65535
                        ),
                    ),
                },
                initial=network,
                interface=TTYPrompter(keep_current=True),
                mode="all",
            )
        except PromptCancelled:
            print("Cancelled; no file was written.")
        else:
            store.save(settings)
            print(store.load())
            print("This demonstration file is removed on exit.")


if __name__ == "__main__":
    main()

A custom interface without a terminal

This deterministic prompter can also serve as a starting point for application tests. It implements the two public protocol methods and cancels when answers run out. The example returns {'enabled': False, 'retries': 3} and records one recoverable error. It never logs raw input or current secret values.

"""Run deterministic collection through the public Prompter protocol."""

from collections.abc import Iterable

from upref import ConfigValue, Field, collect, parse_bool


class ScriptedPrompter:
    """Supply scripted answers, cancelling cleanly when they are exhausted."""

    def __init__(self, answers: Iterable[str | None]) -> None:
        """Keep an iterator without inspecting or logging answer values."""
        self._answers = iter(answers)
        self.errors: list[str] = []

    def ask(self, name: str, field: Field, current: ConfigValue) -> str | None:
        """Return the next raw answer without exposing current secret values."""
        return next(self._answers, None)

    def show_error(self, message: str) -> None:
        """Capture recoverable errors for the application's presentation layer."""
        self.errors.append(message)


def main() -> None:
    """Demonstrate parser retry and a detached structured result without a UI."""
    prompter = ScriptedPrompter(["maybe", "no", "3"])
    settings = collect(
        {
            "enabled": Field("Enabled", parser=parse_bool),
            "retries": Field("Retries", parser=int),
        },
        interface=prompter,
    )
    print(settings)
    print(f"Recoverable errors: {prompter.errors}")


if __name__ == "__main__":
    main()

Application constraints and typed settings

Upref validates representability; the application validates its business rules. This recipe checks loaded values even when no interactive fields are missing. It rejects a boolean used as a port, despite Python’s bool being a subclass of int. Dataclasses must be converted to plain mappings before saving. Unknown keys are intentionally omitted by this fixed application model; preserve the raw mapping if your application needs extension keys.

"""Validate stored values against an application-specific dataclass."""

from dataclasses import asdict, dataclass
from pathlib import Path
from tempfile import TemporaryDirectory

from upref import Config, ConfigStore


@dataclass(frozen=True)
class Settings:
    """Describe the application model independently of the storage format."""

    host: str = "localhost"
    port: int = 8080
    enabled: bool = False

    @classmethod
    def from_config(cls, data: Config) -> "Settings":
        """Reject invalid saved values, including bool masquerading as int."""
        host, port, enabled = data["host"], data["port"], data["enabled"]
        if not isinstance(host, str) or not host.strip():
            raise ValueError("host must be non-empty text")
        if type(port) is not int or not 1 <= port <= 65535:
            raise ValueError("port must be an integer between 1 and 65535")
        if not isinstance(enabled, bool):
            raise ValueError("enabled must be a boolean")
        return cls(host=host, port=port, enabled=enabled)


def main() -> None:
    """Apply defaults, validate application constraints, and persist plain data."""
    with TemporaryDirectory(prefix="upref-typed-") as directory:
        store = ConfigStore("typed-demo", directory=Path(directory).resolve())
        raw = store.load(defaults=asdict(Settings()))
        settings = Settings.from_config(raw)
        store.save(asdict(settings))
        print(settings)


if __name__ == "__main__":
    main()

Versioning an application’s configuration

This is independent of Upref’s v1 import. An application-owned schema version controls an idempotent transformation. It preserves unrelated keys, rejects future versions and ambiguous input, and leaves the original mapping intact. Coordinate production migrations with other writers and keep a backup when rollback is required; a save is atomic but not a transaction with other files.

"""Upgrade an application's own configuration schema independently of Upref v1."""

from copy import deepcopy
from pathlib import Path
from tempfile import TemporaryDirectory

from upref import Config, ConfigStore


def upgrade(data: Config) -> Config:
    """Return an upgraded copy, rejecting unknown or ambiguous schema versions."""
    result = deepcopy(data)
    version = result.get("schema_version", 1)
    if type(version) is not int or version not in (1, 2):
        raise ValueError(f"Unsupported schema version: {version!r}")
    if version == 1:
        if "network" in result:
            raise ValueError(
                "Version 1 unexpectedly contains network; inspect manually"
            )
        host = result.pop("host", "localhost")
        port = result.pop("port", 8080)
        if not isinstance(host, str) or type(port) is not int:
            raise ValueError("Version 1 requires a string host and an integer port")
        result["network"] = {"host": host, "port": port}
        result["schema_version"] = 2
    return result


def main() -> None:
    """Migrate a demonstration file and show that a second upgrade is harmless."""
    with TemporaryDirectory(prefix="upref-upgrade-") as directory:
        store = ConfigStore("upgrade-demo", directory=Path(directory).resolve())
        store.save({"host": "localhost", "port": 8080, "theme": "dark"})
        original = store.load()
        migrated = upgrade(original)
        # In production, retain a backup and coordinate with other writers.
        store.save(migrated)
        assert upgrade(store.load()) == migrated
        print(f"Before: {original}")
        print(f"After: {store.load()}")


if __name__ == "__main__":
    main()

Recovering from malformed YAML

A missing file may use defaults. A malformed existing file needs an explicit repair decision. This example reports a duplicate key with its source location and confirms the original bytes are still available.

"""Report a malformed YAML file without silently replacing the user's data."""

from pathlib import Path
from tempfile import TemporaryDirectory

from upref import ConfigFormatError, ConfigReadError, ConfigStore


def main() -> None:
    """Demonstrate actionable diagnostics and preservation of the original file."""
    with TemporaryDirectory(prefix="upref-errors-") as directory:
        store = ConfigStore("errors-demo", directory=Path(directory).resolve())
        store.path.write_text("port: 8080\nport: 9000\n", encoding="utf-8")
        original = store.path.read_bytes()
        try:
            store.load()
        except ConfigFormatError as error:
            print(f"Fix the YAML file before retrying: {error}")
        except ConfigReadError as error:
            print(f"Check the file path and access rights: {error}")
        assert store.path.read_bytes() == original
        print("The original file was preserved.")


if __name__ == "__main__":
    main()