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 |
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.