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 |
|---|---|
|
Human-readable field name. Interfaces fall back to the schema key when it is empty. |
|
Optional explanatory text displayed with the prompt. |
|
When true, an empty string is rejected and an existing empty string is
missing in |
|
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. |
|
Converts raw text to a supported configuration value. The default is
|
|
Receives the parsed value. Returning |
|
Keyword-only callable converting a current value to parser-compatible
text. Defaults to |
Field definitions are checked at construction: labels and descriptions must be strings, flags must be booleans, and callbacks must be callable. This catches configuration mistakes before a user interface is opened.
Use parse_bool() for textual boolean answers, rather than
bool (which treats every non-empty string, including "false", as true).
It accepts yes/no, true/false, on/off, y/n, and 1/0 after
trimming whitespace and ignoring case. Unknown input raises ValueError
and is therefore retried by collect.
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.
The schema applies to top-level keys; dots in names are literal characters, not paths. To edit a nested mapping, collect that section and put the result back into the complete configuration before saving. See Practical recipes.
Fields skipped in missing mode are not parsed or checked by their field
validators. load checks the configuration data model, not application
constraints such as port ranges. Validate loaded settings separately when
those constraints matter (see the dataclass example in Practical recipes).
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.
The terminal indicates whether a field is required or optional and reminds the user how to cancel. To allow Enter to retain a current value:
from upref import Field, collect, parse_bool
from upref.tty import TTYPrompter
values = collect(
{"enabled": Field("Enabled", parser=parse_bool)},
{"enabled": False},
interface=TTYPrompter(keep_current=True),
mode="all",
)
Value reuse is opt-in. The formatter produces text which is parsed and
validated again; False and 0 can be retained. Secret values and
None are never reused. With the default keep_current=False, Enter
submits empty text, allowing an optional string to be cleared. With reuse
enabled, an empty current value still follows the field’s required rule.
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.
Dialogs show required/optional status. For complex values, provide a formatter
whose output the parser can read; Python’s default string representation of
a list containing booleans or None is not JSON:
import json
json_field = Field(
"Options", parser=json.loads, formatter=json.dumps,
validator=lambda value: isinstance(value, list),
)
Call the GUI from the desktop application’s main thread. The automated suite uses a wxPython substitute to verify dialog arguments and resource ownership; real window rendering should also be checked in a desktop session.
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.