Interactive collection

Interactive collection is an optional layer above the configuration value model. A schema describes how to ask for values, while collect() returns a new mapping. The caller decides whether and when to save it.

Defining fields

Each schema entry maps a configuration key to an immutable Field:

from upref import Field

schema = {
    "service_url": Field(
        label="Service URL",
        description="For example: https://api.example.org",
    ),
    "timeout": Field(
        label="Timeout in seconds",
        parser=int,
        validator=lambda value: isinstance(value, int) and value > 0,
    ),
    "note": Field(
        label="Optional note",
        required=False,
    ),
}

A secret field can be declared separately when an application needs to collect a credential for immediate use or transfer to a real secret store:

token_field = Field(label="API token", secret=True)

Do not add the resulting plaintext token to a mapping passed to save().

Field options have the following behavior:

Option

Meaning

label

Human-readable field name. Interfaces fall back to the schema key when it is empty.

description

Optional explanatory text displayed with the prompt.

required

When true, an empty string is rejected and an existing empty string is missing in mode="missing".

secret

Requests password-style input from compatible interfaces and asks them to hide the current value. It does not encrypt a saved value; see Security and secrets.

parser

Converts raw text to a supported configuration value. The default is str.

validator

Receives the parsed value. Returning False or raising ValueError displays an error and asks again. Returning True or None accepts the value.

The parser result is validated against the configuration value model. A parser should raise ValueError for user-correctable input; unexpected exception types propagate to the application. For a secret field, Upref replaces parser or validator ValueError text with a generic message so raw input is not echoed by the bundled error display.

Collecting missing or all values

Use an existing configuration as initial and save only after collection has succeeded:

from upref import ConfigStore, PromptCancelled, collect

store = ConfigStore("my-app")

try:
    values = collect(
        schema,
        initial=store.load(),
        interface="tty",
        mode="missing",
    )
except PromptCancelled:
    print("No changes saved")
else:
    store.save(values)

mode="missing" asks for a field when its key is absent, its value is None, or its required string is empty. False, 0, empty lists, empty dictionaries, and optional empty strings are already populated.

mode="all" asks every schema field and supplies its current value to the interface. Keys in initial that are not in the schema are preserved in both modes. The input mapping is never mutated, and the result is detached.

When no field needs to be asked, collect returns immediately and does not initialize the selected interface. This permits an application to specify interface="gui" without requiring wxPython when all values are already present.

Terminal interface

interface="tty" is the default and has no optional dependency. It prints the label and description, shows a non-secret current value, and reads one line with input. Secret fields use getpass.getpass and never display the current value. Non-echoing input depends on terminal support; the standard library can warn and fall back to echoed input when it cannot control echo.

End-of-file and Ctrl+C while reading are treated as cancellation and become PromptCancelled.

Applications that need direct control can instantiate upref.tty.TTYPrompter and inject input, password, and output functions. Passing this object as interface also satisfies the Prompter protocol.

Graphical interface

Install the optional dependency and select the interface explicitly:

pip install "upref[gui]"
values = collect(schema, initial=store.load(), interface="gui")

The GUI uses wxPython modal text-entry dialogs. Cancelling a dialog raises PromptCancelled. If wxPython is missing or cannot initialize a GUI, Upref raises PromptUnavailableError.

When collect creates the built-in GUI object, it also closes it. Code that passes its own upref.gui.GuiPrompter instance owns that instance and should close it, preferably with its context manager:

from upref.gui import GuiPrompter

with GuiPrompter(title="Application preferences") as prompter:
    values = collect(schema, initial=store.load(), interface=prompter)

Custom interfaces

A custom interface implements the two-method Prompter protocol:

class ApplicationPrompter:
    def ask(self, name, field, current):
        """Return raw text, or None to cancel."""
        ...

    def show_error(self, message):
        """Present a parser or validator error."""
        ...

values = collect(schema, initial={}, interface=ApplicationPrompter())

Custom prompters are useful for integrating an application’s existing UI and for deterministic tests. Upref does not call close on caller-owned prompters. A custom implementation receives current in plaintext even for a secret field. It is responsible for checking field.secret, avoiding display or prefill of that value, protecting diagnostic logs, and using an appropriate password-entry control.