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()