Using ConfigStore

ConfigStore represents one YAML file. Constructing a store validates and resolves its path, but does not create a directory or read the file.

from upref import ConfigStore

store = ConfigStore(
    "my-application",
    filename="config.yaml",
    app_author="Example Corp",
    roaming=False,
)

The app_name, filename, and path properties expose the resolved identity. See Configuration paths for platform and portable locations.

Loading values

Call load() whenever the application needs a fresh snapshot:

config = store.load()

A missing, zero-byte, whitespace-only, comment-only, or explicit YAML null document represents an empty mapping. Valid YAML is decoded as UTF-8 and normalized to the supported value model. Invalid YAML, invalid UTF-8, a non-mapping root, and non-string keys are reported rather than silently replaced.

PyYAML accepts duplicate mapping keys and keeps the last occurrence. Upref does not currently add a duplicate-key check, so applications that permit manual editing should treat duplicate keys as ambiguous and may validate them with a stricter YAML loader before calling Upref.

Using defaults

Pass defaults when the application has values that need not be present in the file:

config = store.load(
    defaults={
        "theme": "dark",
        "network": {"host": "localhost", "port": 8080},
    }
)

Saved values override defaults according to the recursive merge rules. Defaults are copied and never modified. Supplying defaults does not write them; call store.save(config) if the fully resolved mapping should become persistent.

Saving the complete mapping

save() validates the complete mapping and replaces the complete YAML document:

config["theme"] = "light"
store.save(config)

Validation and YAML serialization happen before the destination directory or temporary file is created. Unsupported values therefore cannot damage an existing configuration. A valid save attempt creates the parent directory as needed before opening its temporary file; the directory can therefore remain even if a later filesystem operation fails.

The document is written in UTF-8 with Unicode characters preserved and keys kept in insertion order. YAML comments, anchors, aliases, quoting choices, and custom formatting are not part of the configuration model and are not preserved on save.

Applying selected changes

update() is a convenience read, recursive merge, and save operation:

updated = store.update(
    {
        "network": {"port": 9000},
        "notifications": False,
    }
)

The returned mapping is exactly the merged mapping that was saved. Existing nested keys are retained, while lists and scalar values are replaced. If the file does not exist, the normalized changes become its initial content.

update does not accept defaults. Load and save explicitly when defaults must participate in the persisted result:

config = store.load(defaults=application_defaults)
config["theme"] = "light"
store.save(config)

Existence and deletion

exists() reports whether the resolved path currently identifies a regular file. delete() deletes only that file and returns True when a file was removed or False when it was already absent:

if store.exists():
    removed = store.delete()

These are separate filesystem operations. Another process may change the file between an exists check and a later load, save, or delete, so callers should still handle the operation’s documented exceptions.

Atomicity and concurrent access

Saving uses this sequence:

  1. validate and serialize all data in memory;

  2. create a uniquely named temporary file in the destination directory;

  3. write UTF-8 YAML, flush Python and operating-system file buffers, and call fsync;

  4. on POSIX, set the temporary file mode to 0600;

  5. atomically replace the destination with os.replace;

  6. attempt to remove any remaining temporary file after failure.

Because the temporary file is on the same filesystem, readers see either the old complete document or the new complete document, not a partially written one. If replacement fails, the previous file remains available. A uniquely named temporary file can remain if both the primary operation and best-effort cleanup fail; Upref never treats that file as configuration.

Upref deliberately provides no process or thread lock. The last successful os.replace wins. In particular, two simultaneous update calls can both read the same old value and one can overwrite the other’s changes. Applications that need coordinated read-modify-write transactions must place an appropriate lock around the whole operation.

Atomic replacement also does not turn YAML into a database: it provides file integrity, not conflict detection, revision history, or multi-file transactions.