upref 2.0.1__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- upref-2.2.0/.gitattributes +3 -0
- upref-2.2.0/CHANGELOG.md +92 -0
- upref-2.2.0/MANIFEST.in +7 -0
- {upref-2.0.1 → upref-2.2.0}/PKG-INFO +77 -14
- {upref-2.0.1 → upref-2.2.0}/README.md +74 -13
- upref-2.2.0/docs/api.rst +110 -0
- upref-2.2.0/docs/changelog.md +2 -0
- upref-2.2.0/docs/concepts.rst +109 -0
- upref-2.2.0/docs/conf.py +52 -0
- upref-2.2.0/docs/development.rst +163 -0
- upref-2.2.0/docs/errors.rst +111 -0
- upref-2.2.0/docs/examples.rst +188 -0
- upref-2.2.0/docs/index.rst +57 -0
- upref-2.2.0/docs/layout/extra.css +4 -0
- upref-2.2.0/docs/layout/tower.png +0 -0
- upref-2.2.0/docs/legacy_api.rst +57 -0
- upref-2.2.0/docs/license_link.md +4 -0
- upref-2.2.0/docs/migration.rst +162 -0
- upref-2.2.0/docs/paths.rst +114 -0
- upref-2.2.0/docs/prompting.rst +250 -0
- upref-2.2.0/docs/readme_link.md +5 -0
- upref-2.2.0/docs/recipes.rst +106 -0
- upref-2.2.0/docs/releasing.rst +92 -0
- upref-2.2.0/docs/review.rst +135 -0
- upref-2.2.0/docs/security.rst +106 -0
- upref-2.2.0/docs/storage.rst +173 -0
- upref-2.2.0/docs/troubleshooting.rst +87 -0
- upref-2.2.0/docs/user_preferences.rst +172 -0
- upref-2.2.0/examples/README.md +97 -0
- upref-2.2.0/examples/account_preferences.py +50 -0
- upref-2.2.0/examples/apply_preferences.py +57 -0
- upref-2.2.0/examples/backup_restore.py +35 -0
- upref-2.2.0/examples/basic_store.py +16 -0
- upref-2.2.0/examples/boolean_collection.py +19 -0
- upref-2.2.0/examples/custom_interface.py +40 -0
- upref-2.2.0/examples/defaults_and_update.py +30 -0
- upref-2.2.0/examples/edit_settings.py +38 -0
- upref-2.2.0/examples/environment_profiles.py +85 -0
- upref-2.2.0/examples/first_run_preferences.py +69 -0
- upref-2.2.0/examples/gui_collection.py +39 -0
- upref-2.2.0/examples/handle_errors.py +26 -0
- upref-2.2.0/examples/import_export_preferences.py +89 -0
- upref-2.2.0/examples/migrate_v1.py +18 -0
- upref-2.2.0/examples/migration_preview.py +36 -0
- upref-2.2.0/examples/multiple_projects.py +32 -0
- upref-2.2.0/examples/nested_collection.py +49 -0
- upref-2.2.0/examples/portable_store.py +23 -0
- upref-2.2.0/examples/project_variables.py +53 -0
- upref-2.2.0/examples/recent_files.py +44 -0
- upref-2.2.0/examples/reset_preferences.py +46 -0
- upref-2.2.0/examples/schema_upgrade.py +45 -0
- upref-2.2.0/examples/session_overrides.py +59 -0
- upref-2.2.0/examples/tty_collection.py +29 -0
- upref-2.2.0/examples/typed_settings.py +42 -0
- upref-2.2.0/examples/window_preferences.py +44 -0
- upref-2.2.0/make.bat +140 -0
- {upref-2.0.1 → upref-2.2.0}/pyproject.toml +5 -1
- upref-2.2.0/pytest.ini +45 -0
- upref-2.2.0/scripts/add_license_headers.py +520 -0
- upref-2.2.0/scripts/bootstrap.ps1 +210 -0
- upref-2.2.0/scripts/check_installed_package.py +41 -0
- upref-2.2.0/tests/integration/_gui_scenario.py +165 -0
- upref-2.2.0/tests/integration/test_gui.py +29 -0
- {upref-2.0.1 → upref-2.2.0}/tests/test_core.py +12 -2
- upref-2.2.0/tests/test_examples.py +147 -0
- {upref-2.0.1 → upref-2.2.0}/tests/test_legacy.py +102 -0
- {upref-2.0.1 → upref-2.2.0}/tests/test_paths.py +20 -0
- upref-2.2.0/tests/test_preference_examples.py +210 -0
- {upref-2.0.1 → upref-2.2.0}/tests/test_prompt.py +82 -3
- upref-2.2.0/tests/test_properties.py +92 -0
- {upref-2.0.1 → upref-2.2.0}/tests/test_public_api.py +2 -1
- {upref-2.0.1 → upref-2.2.0}/tests/test_storage.py +142 -6
- {upref-2.0.1 → upref-2.2.0}/tests/test_tty.py +65 -0
- {upref-2.0.1 → upref-2.2.0}/tests/test_types.py +54 -0
- {upref-2.0.1 → upref-2.2.0}/upref/__init__.py +3 -2
- {upref-2.0.1 → upref-2.2.0}/upref/_paths.py +4 -1
- {upref-2.0.1 → upref-2.2.0}/upref/_storage.py +93 -29
- {upref-2.0.1 → upref-2.2.0}/upref/_types.py +18 -7
- {upref-2.0.1 → upref-2.2.0}/upref/core.py +26 -8
- {upref-2.0.1 → upref-2.2.0}/upref/gui.py +5 -4
- {upref-2.0.1 → upref-2.2.0}/upref/legacy.py +26 -8
- {upref-2.0.1 → upref-2.2.0}/upref/prompt.py +54 -4
- {upref-2.0.1 → upref-2.2.0}/upref/tty.py +25 -3
- {upref-2.0.1 → upref-2.2.0}/upref.egg-info/PKG-INFO +77 -14
- upref-2.2.0/upref.egg-info/SOURCES.txt +97 -0
- {upref-2.0.1 → upref-2.2.0}/upref.egg-info/requires.txt +2 -0
- upref-2.0.1/upref.egg-info/SOURCES.txt +0 -34
- {upref-2.0.1 → upref-2.2.0}/LICENSE.md +0 -0
- {upref-2.0.1 → upref-2.2.0}/setup.cfg +0 -0
- {upref-2.0.1 → upref-2.2.0}/setup.py +0 -0
- {upref-2.0.1 → upref-2.2.0}/tests/test_documentation.py +0 -0
- {upref-2.0.1 → upref-2.2.0}/tests/test_license_headers.py +0 -0
- {upref-2.0.1 → upref-2.2.0}/tests/test_merge.py +0 -0
- {upref-2.0.1 → upref-2.2.0}/upref/_merge.py +0 -0
- {upref-2.0.1 → upref-2.2.0}/upref/errors.py +0 -0
- {upref-2.0.1 → upref-2.2.0}/upref/py.typed +0 -0
- {upref-2.0.1 → upref-2.2.0}/upref/resources/__init__.py +0 -0
- {upref-2.0.1 → upref-2.2.0}/upref/resources/tower.ico +0 -0
- {upref-2.0.1 → upref-2.2.0}/upref.egg-info/dependency_links.txt +0 -0
- {upref-2.0.1 → upref-2.2.0}/upref.egg-info/top_level.txt +0 -0
upref-2.2.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 2.2.0 - 2026-09-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Ten runnable user-preference scenarios: first-run setup, Apply/Cancel, resets,
|
|
8
|
+
recent documents, window geometry, account settings, CLI session overrides,
|
|
9
|
+
portable JSON import/export, backup/restore, and legacy migration preview.
|
|
10
|
+
The catalog now contains 26 examples, with a dedicated walkthrough guide.
|
|
11
|
+
- Explicit `source_format="auto" | "raw" | "descriptors"` selection and
|
|
12
|
+
`dry_run=True` previews for legacy migration, retaining the default heuristic
|
|
13
|
+
and target preflight checks.
|
|
14
|
+
- Hypothesis properties for persistence, copying, merging, and failed writes.
|
|
15
|
+
- Native wxPython integration scenarios with process timeouts and a dedicated
|
|
16
|
+
Windows CI job; minimum runtime dependency testing on Python 3.10.
|
|
17
|
+
- Functional installed-wheel and resource checks in CI and release automation.
|
|
18
|
+
- A release-based deprecation schedule: v1 wrappers remain through 2.x and are
|
|
19
|
+
scheduled for removal in 3.0. Explicit migration remains supported.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Preserve Unicode NEL (U+0085) in YAML keys and values instead of folding it
|
|
24
|
+
into a space; ordinary Unicode remains readable.
|
|
25
|
+
- Reject non-scalar YAML keys carrying scalar tags with `ConfigFormatError`
|
|
26
|
+
and a source location instead of leaking a `TypeError`.
|
|
27
|
+
- Reject null characters in configuration directories during construction.
|
|
28
|
+
- Normalize string subclass keys to plain strings, preserving their underlying
|
|
29
|
+
text so string-backed enum keys can be saved. Scalar enum values still need
|
|
30
|
+
explicit conversion; normalized key collisions are rejected.
|
|
31
|
+
- Measure branch coverage as well as statement coverage, retaining the 100%
|
|
32
|
+
gate and covering application reuse and pre-write failures.
|
|
33
|
+
- Exercise POSIX and Windows permission branches on every platform so the
|
|
34
|
+
coverage gate behaves consistently on Linux, macOS, and Windows.
|
|
35
|
+
|
|
36
|
+
### Compatibility
|
|
37
|
+
|
|
38
|
+
- Python 3.10 and later remain supported, with no new runtime dependency.
|
|
39
|
+
- Existing migration calls retain their behavior; format selection and previews
|
|
40
|
+
are optional. Deprecated v1 wrappers remain available throughout 2.x.
|
|
41
|
+
- Malformed YAML keys and invalid configuration directories now consistently
|
|
42
|
+
raise the documented configuration exceptions. Duplicate normalized keys are
|
|
43
|
+
rejected instead of silently replacing an existing value.
|
|
44
|
+
|
|
45
|
+
## 2.1.0
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- Public `parse_bool` parser for yes/no, true/false, on/off, y/n, and 1/0 input.
|
|
50
|
+
- Keyword-only `Field.formatter` to format current values for terminal display
|
|
51
|
+
and GUI prefill, including JSON lists and mappings.
|
|
52
|
+
- Opt-in `TTYPrompter(keep_current=True)` to retain a non-secret current value
|
|
53
|
+
with Enter, while still parsing and validating it.
|
|
54
|
+
- Nine runnable examples covering portable storage, boolean input, editing,
|
|
55
|
+
GUI collection, custom interfaces, nested settings, application validation,
|
|
56
|
+
schema upgrades, and error handling; 16 examples are now available.
|
|
57
|
+
- Practical recipes, troubleshooting, and a documented code review.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- Reject duplicate explicit YAML keys with source locations while preserving
|
|
62
|
+
YAML merge defaults and explicit overrides.
|
|
63
|
+
- Report invalid YAML scalar construction and excessive recursion during
|
|
64
|
+
parsing, normalization, or serialization as `ConfigFormatError`.
|
|
65
|
+
- Preserve the documented `ConfigReadError` behavior for file inspection
|
|
66
|
+
failures in `ConfigStore.exists()`.
|
|
67
|
+
- Reject malformed fields, schemas, and custom prompter contracts early.
|
|
68
|
+
- Clarify required/optional input and cancellation in the bundled interfaces.
|
|
69
|
+
- Include examples, documentation, scripts, and pytest configuration in source
|
|
70
|
+
distributions, with an archive-content check in CI.
|
|
71
|
+
- Align Python checkout line endings with the formatter on Windows.
|
|
72
|
+
|
|
73
|
+
### Compatibility
|
|
74
|
+
|
|
75
|
+
- Files with duplicate explicit YAML keys must be corrected before loading.
|
|
76
|
+
Previous releases silently selected the last value. YAML merge directives
|
|
77
|
+
remain supported.
|
|
78
|
+
- Invalid `Field` definitions now raise `TypeError` at construction. Custom
|
|
79
|
+
prompter methods must be callable, and `ask()` must return text or `None`.
|
|
80
|
+
- Existing positional `Field` arguments remain supported. The formatter and
|
|
81
|
+
terminal value-reuse option are optional; blank input is still submitted
|
|
82
|
+
literally by default. Secret current values are never formatted or reused.
|
|
83
|
+
- Python 3.10 and later remain supported. No new runtime dependency is required.
|
|
84
|
+
|
|
85
|
+
### Validation
|
|
86
|
+
|
|
87
|
+
- 230 tests passed locally on Windows with 100% statement coverage; three
|
|
88
|
+
platform-specific tests were skipped.
|
|
89
|
+
- Ruff, strict mypy, Sphinx, source-distribution tests, wheel persistence smoke
|
|
90
|
+
checks, and distribution metadata validation passed.
|
|
91
|
+
- GUI contracts use a wxPython substitute in the tests; actual window rendering
|
|
92
|
+
was not verified in the local environment.
|
upref-2.2.0/MANIFEST.in
ADDED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upref
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Small, explicit, and safe per-user configuration storage
|
|
5
5
|
Author: Florent Tournois
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,6 +25,7 @@ Requires-Dist: PyYAML>=6.0
|
|
|
25
25
|
Provides-Extra: gui
|
|
26
26
|
Requires-Dist: wxPython>=4.2; extra == "gui"
|
|
27
27
|
Provides-Extra: test
|
|
28
|
+
Requires-Dist: hypothesis>=6.100; extra == "test"
|
|
28
29
|
Requires-Dist: mypy>=2; extra == "test"
|
|
29
30
|
Requires-Dist: pytest>=8; extra == "test"
|
|
30
31
|
Requires-Dist: pytest-cov>=5; extra == "test"
|
|
@@ -36,6 +37,7 @@ Requires-Dist: sphinx>=7; extra == "docs"
|
|
|
36
37
|
Requires-Dist: sphinx-rtd-theme>=2; extra == "docs"
|
|
37
38
|
Provides-Extra: dev
|
|
38
39
|
Requires-Dist: build>=1.2; extra == "dev"
|
|
40
|
+
Requires-Dist: hypothesis>=6.100; extra == "dev"
|
|
39
41
|
Requires-Dist: mypy>=2; extra == "dev"
|
|
40
42
|
Requires-Dist: myst-parser>=2; extra == "dev"
|
|
41
43
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
@@ -113,6 +115,9 @@ config = store.update({
|
|
|
113
115
|
```
|
|
114
116
|
|
|
115
117
|
Nested dictionaries are merged. Lists and scalar values are replaced.
|
|
118
|
+
Duplicate explicit YAML keys are rejected with a file location instead of
|
|
119
|
+
silently discarding earlier values. YAML merge directives may still provide
|
|
120
|
+
defaults that explicit keys override.
|
|
116
121
|
`store.exists()` checks for the file, while `store.delete()` removes only that
|
|
117
122
|
file and reports whether it existed.
|
|
118
123
|
|
|
@@ -192,6 +197,32 @@ In `missing` mode, absent values, `None`, and required empty strings are
|
|
|
192
197
|
requested. `False` and `0` already count as values. Use `mode="all"` to ask
|
|
193
198
|
for every field, or `interface="gui"` after installing `upref[gui]`.
|
|
194
199
|
|
|
200
|
+
For boolean input, use the public `parse_bool` parser; it accepts `yes/no`,
|
|
201
|
+
`true/false`, `on/off`, `y/n`, and `1/0`, ignoring case and whitespace.
|
|
202
|
+
Python's `bool("false")` returns `True` and is unsuitable for this purpose.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from upref import Field, collect, parse_bool
|
|
206
|
+
from upref.tty import TTYPrompter
|
|
207
|
+
|
|
208
|
+
values = collect(
|
|
209
|
+
{"enabled": Field("Enable notifications", parser=parse_bool)},
|
|
210
|
+
initial={"enabled": False},
|
|
211
|
+
interface=TTYPrompter(keep_current=True),
|
|
212
|
+
mode="all",
|
|
213
|
+
)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
With `keep_current=True`, Enter reuses a current non-secret value and runs it
|
|
217
|
+
through the parser and validator again. By default, blank input remains blank.
|
|
218
|
+
For structured input, pair a parser with a compatible keyword-only formatter:
|
|
219
|
+
`Field("Tags", parser=json.loads, formatter=json.dumps)` (after `import json`).
|
|
220
|
+
The GUI uses this formatter to prefill existing values correctly.
|
|
221
|
+
|
|
222
|
+
`collect` validates newly entered values. Existing values skipped in `missing`
|
|
223
|
+
mode are not passed through field validators; validate application constraints
|
|
224
|
+
after loading when stored files may have been edited manually.
|
|
225
|
+
|
|
195
226
|
## Storage guarantees and limits
|
|
196
227
|
|
|
197
228
|
Upref writes UTF-8 YAML to a temporary file in the destination directory,
|
|
@@ -216,10 +247,18 @@ store = ConfigStore("my-application")
|
|
|
216
247
|
config = store.import_legacy("my_personnal_data")
|
|
217
248
|
```
|
|
218
249
|
|
|
250
|
+
For ambiguous files, choose ``source_format="raw"`` to retain the entire
|
|
251
|
+
mapping, or ``source_format="descriptors"`` to extract v1 values explicitly.
|
|
252
|
+
Add ``dry_run=True`` to inspect the conversion without writing. Previewing
|
|
253
|
+
still performs source and target preflight checks. The default ``"auto"``
|
|
254
|
+
format retains the historical heuristic.
|
|
255
|
+
|
|
219
256
|
The source file is left untouched, and using it as the v2 target is rejected.
|
|
220
257
|
Migration checks for an existing v2 file unless `overwrite=True` is passed;
|
|
221
258
|
because this preflight check is not locked, applications with concurrent
|
|
222
259
|
writers must coordinate the migration externally.
|
|
260
|
+
Deprecated v1 wrappers remain available throughout 2.x and are scheduled for
|
|
261
|
+
removal in 3.0; explicit file migration remains supported.
|
|
223
262
|
|
|
224
263
|
## Security
|
|
225
264
|
|
|
@@ -233,19 +272,43 @@ reference in Upref.
|
|
|
233
272
|
## Documentation and examples
|
|
234
273
|
|
|
235
274
|
The complete user guide and API reference are available on
|
|
236
|
-
[Read the Docs](https://upref.readthedocs.io/).
|
|
237
|
-
[
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
275
|
+
[Read the Docs](https://upref.readthedocs.io/). The
|
|
276
|
+
[example catalog](docs/examples.rst) lists 26 runnable programs by difficulty,
|
|
277
|
+
input method, and file effects. Start with these:
|
|
278
|
+
|
|
279
|
+
| Level | Example | What it demonstrates |
|
|
280
|
+
| --- | --- | --- |
|
|
281
|
+
| Simple | `portable_store.py` | Defaults, save/load, and updates in a temporary directory |
|
|
282
|
+
| Simple | `boolean_collection.py` | Boolean parsing, optional input, and cancellation |
|
|
283
|
+
| Simple | `first_run_preferences.py` | Initial user setup and reuse on the next startup |
|
|
284
|
+
| Intermediate | `apply_preferences.py` | Edit a draft, then confirm or discard changes |
|
|
285
|
+
| Intermediate | `reset_preferences.py` | Restore one preference, a section, or all defaults |
|
|
286
|
+
| Intermediate | `session_overrides.py` | CLI overrides with explicit `--remember` persistence |
|
|
287
|
+
| Advanced | `import_export_preferences.py` | Validated preference transfer with local history retained |
|
|
288
|
+
| Intermediate | `edit_settings.py` | Enter to keep values and editable JSON lists |
|
|
289
|
+
| Intermediate | `gui_collection.py` | GUI ownership, formatted prefill, and cancellation |
|
|
290
|
+
| Intermediate | `handle_errors.py` | Reporting malformed YAML while retaining the file |
|
|
291
|
+
| Advanced | `nested_collection.py` | Editing a subsection before an explicit save |
|
|
292
|
+
| Advanced | `custom_interface.py` | A deterministic custom prompter with validation retries |
|
|
293
|
+
| Advanced | `typed_settings.py` | Application validation with a dataclass |
|
|
294
|
+
| Advanced | `schema_upgrade.py` | An idempotent application schema migration |
|
|
295
|
+
|
|
296
|
+
The new demonstrations use memory or automatically cleaned temporary
|
|
297
|
+
directories. Earlier examples that demonstrate persistent settings use named
|
|
298
|
+
per-user directories; their effects are listed in the catalog. Run examples
|
|
299
|
+
from an installed checkout (`python -m pip install -e .`):
|
|
300
|
+
|
|
301
|
+
```console
|
|
302
|
+
python examples/portable_store.py
|
|
303
|
+
python examples/custom_interface.py
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The [user preference walkthroughs](docs/user_preferences.rst) also cover recent
|
|
307
|
+
documents, window geometry, multiple accounts, backup/restore, and previewing
|
|
308
|
+
legacy migration. Every scenario in that guide uses temporary files.
|
|
309
|
+
|
|
310
|
+
See also the [troubleshooting guide](docs/troubleshooting.rst) and the
|
|
311
|
+
[code review and compatibility notes](docs/review.rst).
|
|
249
312
|
|
|
250
313
|
## Development with `.venv`
|
|
251
314
|
|
|
@@ -63,6 +63,9 @@ config = store.update({
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Nested dictionaries are merged. Lists and scalar values are replaced.
|
|
66
|
+
Duplicate explicit YAML keys are rejected with a file location instead of
|
|
67
|
+
silently discarding earlier values. YAML merge directives may still provide
|
|
68
|
+
defaults that explicit keys override.
|
|
66
69
|
`store.exists()` checks for the file, while `store.delete()` removes only that
|
|
67
70
|
file and reports whether it existed.
|
|
68
71
|
|
|
@@ -142,6 +145,32 @@ In `missing` mode, absent values, `None`, and required empty strings are
|
|
|
142
145
|
requested. `False` and `0` already count as values. Use `mode="all"` to ask
|
|
143
146
|
for every field, or `interface="gui"` after installing `upref[gui]`.
|
|
144
147
|
|
|
148
|
+
For boolean input, use the public `parse_bool` parser; it accepts `yes/no`,
|
|
149
|
+
`true/false`, `on/off`, `y/n`, and `1/0`, ignoring case and whitespace.
|
|
150
|
+
Python's `bool("false")` returns `True` and is unsuitable for this purpose.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from upref import Field, collect, parse_bool
|
|
154
|
+
from upref.tty import TTYPrompter
|
|
155
|
+
|
|
156
|
+
values = collect(
|
|
157
|
+
{"enabled": Field("Enable notifications", parser=parse_bool)},
|
|
158
|
+
initial={"enabled": False},
|
|
159
|
+
interface=TTYPrompter(keep_current=True),
|
|
160
|
+
mode="all",
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
With `keep_current=True`, Enter reuses a current non-secret value and runs it
|
|
165
|
+
through the parser and validator again. By default, blank input remains blank.
|
|
166
|
+
For structured input, pair a parser with a compatible keyword-only formatter:
|
|
167
|
+
`Field("Tags", parser=json.loads, formatter=json.dumps)` (after `import json`).
|
|
168
|
+
The GUI uses this formatter to prefill existing values correctly.
|
|
169
|
+
|
|
170
|
+
`collect` validates newly entered values. Existing values skipped in `missing`
|
|
171
|
+
mode are not passed through field validators; validate application constraints
|
|
172
|
+
after loading when stored files may have been edited manually.
|
|
173
|
+
|
|
145
174
|
## Storage guarantees and limits
|
|
146
175
|
|
|
147
176
|
Upref writes UTF-8 YAML to a temporary file in the destination directory,
|
|
@@ -166,10 +195,18 @@ store = ConfigStore("my-application")
|
|
|
166
195
|
config = store.import_legacy("my_personnal_data")
|
|
167
196
|
```
|
|
168
197
|
|
|
198
|
+
For ambiguous files, choose ``source_format="raw"`` to retain the entire
|
|
199
|
+
mapping, or ``source_format="descriptors"`` to extract v1 values explicitly.
|
|
200
|
+
Add ``dry_run=True`` to inspect the conversion without writing. Previewing
|
|
201
|
+
still performs source and target preflight checks. The default ``"auto"``
|
|
202
|
+
format retains the historical heuristic.
|
|
203
|
+
|
|
169
204
|
The source file is left untouched, and using it as the v2 target is rejected.
|
|
170
205
|
Migration checks for an existing v2 file unless `overwrite=True` is passed;
|
|
171
206
|
because this preflight check is not locked, applications with concurrent
|
|
172
207
|
writers must coordinate the migration externally.
|
|
208
|
+
Deprecated v1 wrappers remain available throughout 2.x and are scheduled for
|
|
209
|
+
removal in 3.0; explicit file migration remains supported.
|
|
173
210
|
|
|
174
211
|
## Security
|
|
175
212
|
|
|
@@ -183,19 +220,43 @@ reference in Upref.
|
|
|
183
220
|
## Documentation and examples
|
|
184
221
|
|
|
185
222
|
The complete user guide and API reference are available on
|
|
186
|
-
[Read the Docs](https://upref.readthedocs.io/).
|
|
187
|
-
[
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
223
|
+
[Read the Docs](https://upref.readthedocs.io/). The
|
|
224
|
+
[example catalog](docs/examples.rst) lists 26 runnable programs by difficulty,
|
|
225
|
+
input method, and file effects. Start with these:
|
|
226
|
+
|
|
227
|
+
| Level | Example | What it demonstrates |
|
|
228
|
+
| --- | --- | --- |
|
|
229
|
+
| Simple | `portable_store.py` | Defaults, save/load, and updates in a temporary directory |
|
|
230
|
+
| Simple | `boolean_collection.py` | Boolean parsing, optional input, and cancellation |
|
|
231
|
+
| Simple | `first_run_preferences.py` | Initial user setup and reuse on the next startup |
|
|
232
|
+
| Intermediate | `apply_preferences.py` | Edit a draft, then confirm or discard changes |
|
|
233
|
+
| Intermediate | `reset_preferences.py` | Restore one preference, a section, or all defaults |
|
|
234
|
+
| Intermediate | `session_overrides.py` | CLI overrides with explicit `--remember` persistence |
|
|
235
|
+
| Advanced | `import_export_preferences.py` | Validated preference transfer with local history retained |
|
|
236
|
+
| Intermediate | `edit_settings.py` | Enter to keep values and editable JSON lists |
|
|
237
|
+
| Intermediate | `gui_collection.py` | GUI ownership, formatted prefill, and cancellation |
|
|
238
|
+
| Intermediate | `handle_errors.py` | Reporting malformed YAML while retaining the file |
|
|
239
|
+
| Advanced | `nested_collection.py` | Editing a subsection before an explicit save |
|
|
240
|
+
| Advanced | `custom_interface.py` | A deterministic custom prompter with validation retries |
|
|
241
|
+
| Advanced | `typed_settings.py` | Application validation with a dataclass |
|
|
242
|
+
| Advanced | `schema_upgrade.py` | An idempotent application schema migration |
|
|
243
|
+
|
|
244
|
+
The new demonstrations use memory or automatically cleaned temporary
|
|
245
|
+
directories. Earlier examples that demonstrate persistent settings use named
|
|
246
|
+
per-user directories; their effects are listed in the catalog. Run examples
|
|
247
|
+
from an installed checkout (`python -m pip install -e .`):
|
|
248
|
+
|
|
249
|
+
```console
|
|
250
|
+
python examples/portable_store.py
|
|
251
|
+
python examples/custom_interface.py
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The [user preference walkthroughs](docs/user_preferences.rst) also cover recent
|
|
255
|
+
documents, window geometry, multiple accounts, backup/restore, and previewing
|
|
256
|
+
legacy migration. Every scenario in that guide uses temporary files.
|
|
257
|
+
|
|
258
|
+
See also the [troubleshooting guide](docs/troubleshooting.rst) and the
|
|
259
|
+
[code review and compatibility notes](docs/review.rst).
|
|
199
260
|
|
|
200
261
|
## Development with `.venv`
|
|
201
262
|
|
upref-2.2.0/docs/api.rst
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
.. _api-reference:
|
|
2
|
+
|
|
3
|
+
API reference
|
|
4
|
+
=============
|
|
5
|
+
|
|
6
|
+
.. currentmodule:: upref
|
|
7
|
+
|
|
8
|
+
The objects on this page form the supported Upref v2 API. Start with
|
|
9
|
+
:class:`ConfigStore` for persistence and use :func:`collect` only when the
|
|
10
|
+
application needs interactive input.
|
|
11
|
+
|
|
12
|
+
Public API summary
|
|
13
|
+
------------------
|
|
14
|
+
|
|
15
|
+
.. autosummary::
|
|
16
|
+
|
|
17
|
+
ConfigStore
|
|
18
|
+
Field
|
|
19
|
+
Prompter
|
|
20
|
+
collect
|
|
21
|
+
parse_bool
|
|
22
|
+
UprefError
|
|
23
|
+
ConfigPathError
|
|
24
|
+
ConfigFormatError
|
|
25
|
+
ConfigReadError
|
|
26
|
+
ConfigWriteError
|
|
27
|
+
MigrationError
|
|
28
|
+
PromptCancelled
|
|
29
|
+
PromptUnavailableError
|
|
30
|
+
|
|
31
|
+
Configuration storage
|
|
32
|
+
---------------------
|
|
33
|
+
|
|
34
|
+
.. autoclass:: ConfigStore
|
|
35
|
+
:members:
|
|
36
|
+
:special-members: __init__
|
|
37
|
+
|
|
38
|
+
Configuration types
|
|
39
|
+
-------------------
|
|
40
|
+
|
|
41
|
+
.. py:data:: Config
|
|
42
|
+
|
|
43
|
+
A configuration mapping with string keys and :data:`ConfigValue` values.
|
|
44
|
+
|
|
45
|
+
.. py:data:: ConfigValue
|
|
46
|
+
|
|
47
|
+
A recursive type alias accepting ``None``, ``bool``, ``int``, ``float``,
|
|
48
|
+
``str``, lists of supported values, and dictionaries with string keys.
|
|
49
|
+
|
|
50
|
+
.. py:data:: prompt.PromptMode
|
|
51
|
+
|
|
52
|
+
The collection mode literal: ``"missing"`` or ``"all"``.
|
|
53
|
+
|
|
54
|
+
Interactive schema and protocol
|
|
55
|
+
-------------------------------
|
|
56
|
+
|
|
57
|
+
.. autoclass:: Field
|
|
58
|
+
:members:
|
|
59
|
+
|
|
60
|
+
.. autoclass:: Prompter
|
|
61
|
+
:members:
|
|
62
|
+
|
|
63
|
+
.. autofunction:: collect
|
|
64
|
+
|
|
65
|
+
.. autofunction:: parse_bool
|
|
66
|
+
|
|
67
|
+
Built-in interfaces
|
|
68
|
+
-------------------
|
|
69
|
+
|
|
70
|
+
.. autoclass:: upref.tty.TTYPrompter
|
|
71
|
+
:members:
|
|
72
|
+
:special-members: __init__
|
|
73
|
+
|
|
74
|
+
.. autoclass:: upref.gui.GuiPrompter
|
|
75
|
+
:members:
|
|
76
|
+
:special-members: __init__, __enter__, __exit__
|
|
77
|
+
|
|
78
|
+
Exceptions
|
|
79
|
+
----------
|
|
80
|
+
|
|
81
|
+
.. autoexception:: UprefError
|
|
82
|
+
:show-inheritance:
|
|
83
|
+
|
|
84
|
+
.. autoexception:: ConfigPathError
|
|
85
|
+
:show-inheritance:
|
|
86
|
+
|
|
87
|
+
.. autoexception:: ConfigFormatError
|
|
88
|
+
:show-inheritance:
|
|
89
|
+
|
|
90
|
+
.. autoexception:: ConfigReadError
|
|
91
|
+
:show-inheritance:
|
|
92
|
+
|
|
93
|
+
.. autoexception:: ConfigWriteError
|
|
94
|
+
:show-inheritance:
|
|
95
|
+
|
|
96
|
+
.. autoexception:: MigrationError
|
|
97
|
+
:show-inheritance:
|
|
98
|
+
|
|
99
|
+
.. autoexception:: PromptCancelled
|
|
100
|
+
:show-inheritance:
|
|
101
|
+
|
|
102
|
+
.. autoexception:: PromptUnavailableError
|
|
103
|
+
:show-inheritance:
|
|
104
|
+
|
|
105
|
+
Package metadata
|
|
106
|
+
----------------
|
|
107
|
+
|
|
108
|
+
``upref.__version__`` contains the installed distribution version.
|
|
109
|
+
``upref.__author__``, ``upref.__license__``, and ``upref.__copyright__``
|
|
110
|
+
describe the package's authorship and license.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
.. _concepts:
|
|
2
|
+
|
|
3
|
+
Configuration model
|
|
4
|
+
===================
|
|
5
|
+
|
|
6
|
+
Upref deliberately has a small data model and an explicit lifecycle. An
|
|
7
|
+
application normally performs three independent steps:
|
|
8
|
+
|
|
9
|
+
1. create a :class:`~upref.ConfigStore` and :meth:`~upref.ConfigStore.load`
|
|
10
|
+
values;
|
|
11
|
+
2. optionally use :func:`~upref.collect` to ask for values;
|
|
12
|
+
3. explicitly :meth:`~upref.ConfigStore.save` the final mapping.
|
|
13
|
+
|
|
14
|
+
Loading never opens a terminal or window. Collecting never writes a file.
|
|
15
|
+
This separation makes startup, cancellation, testing, and error recovery
|
|
16
|
+
predictable.
|
|
17
|
+
|
|
18
|
+
.. _config-values:
|
|
19
|
+
|
|
20
|
+
Supported values
|
|
21
|
+
----------------
|
|
22
|
+
|
|
23
|
+
The document root is always a mapping with string keys. Each value can be one
|
|
24
|
+
of the following:
|
|
25
|
+
|
|
26
|
+
.. list-table::
|
|
27
|
+
:header-rows: 1
|
|
28
|
+
:widths: 28 72
|
|
29
|
+
|
|
30
|
+
* - Python value
|
|
31
|
+
- YAML representation
|
|
32
|
+
* - ``None``
|
|
33
|
+
- ``null``
|
|
34
|
+
* - ``bool``
|
|
35
|
+
- ``true`` or ``false``
|
|
36
|
+
* - ``int`` or ``float``
|
|
37
|
+
- A YAML number
|
|
38
|
+
* - ``str``
|
|
39
|
+
- A Unicode YAML string
|
|
40
|
+
* - ``list``
|
|
41
|
+
- A sequence whose items are supported values
|
|
42
|
+
* - ``dict[str, ConfigValue]``
|
|
43
|
+
- A nested mapping with string keys
|
|
44
|
+
|
|
45
|
+
Empty strings, empty containers, ``False``, and ``0`` are valid values. An
|
|
46
|
+
empty document or explicit YAML ``null`` at the root represents an empty
|
|
47
|
+
mapping. A list or non-null scalar at the document root is invalid. Mapping
|
|
48
|
+
keys at every level must be strings. Keys derived from ``str``, including
|
|
49
|
+
string-backed enum members, are normalized to ordinary strings using their
|
|
50
|
+
underlying text. A customized ``__str__`` display does not rename the key.
|
|
51
|
+
If distinct custom keys normalize to the same text, validation rejects the
|
|
52
|
+
collision instead of discarding a value.
|
|
53
|
+
|
|
54
|
+
Values such as ``pathlib.Path``, ``datetime``, tuples, sets, enum
|
|
55
|
+
members, and application classes are not converted implicitly. Convert them
|
|
56
|
+
to a supported representation before calling
|
|
57
|
+
:meth:`~upref.ConfigStore.save`.
|
|
58
|
+
|
|
59
|
+
Cycles are rejected because YAML configuration must be a finite tree. Shared
|
|
60
|
+
mutable objects are copied independently when accepted.
|
|
61
|
+
|
|
62
|
+
Detached values and no cache
|
|
63
|
+
----------------------------
|
|
64
|
+
|
|
65
|
+
Upref validates and copies the complete value tree at its public boundaries.
|
|
66
|
+
Inputs are never mutated, and returned dictionaries do not share nested lists
|
|
67
|
+
or dictionaries with caller-owned defaults or changes.
|
|
68
|
+
|
|
69
|
+
Every :meth:`~upref.ConfigStore.load` reads the file again; a store has no
|
|
70
|
+
implicit configuration cache. Mutating a loaded dictionary therefore changes
|
|
71
|
+
only that Python object until :meth:`~upref.ConfigStore.save` is called.
|
|
72
|
+
|
|
73
|
+
.. _merge-semantics:
|
|
74
|
+
|
|
75
|
+
Recursive merge rules
|
|
76
|
+
---------------------
|
|
77
|
+
|
|
78
|
+
Defaults and updates use the same merge rules:
|
|
79
|
+
|
|
80
|
+
* nested mappings are merged recursively;
|
|
81
|
+
* a value from the overriding mapping wins;
|
|
82
|
+
* lists and scalars are replaced in full, not concatenated;
|
|
83
|
+
* keys found only in the overriding mapping are retained;
|
|
84
|
+
* both inputs and the result remain independent.
|
|
85
|
+
|
|
86
|
+
For example, given these defaults and saved values:
|
|
87
|
+
|
|
88
|
+
.. code-block:: python
|
|
89
|
+
|
|
90
|
+
defaults = {
|
|
91
|
+
"network": {"host": "localhost", "ports": [8000, 8001]},
|
|
92
|
+
"enabled": True,
|
|
93
|
+
}
|
|
94
|
+
saved = {
|
|
95
|
+
"network": {"ports": [9000]},
|
|
96
|
+
"enabled": False,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
the resolved configuration is:
|
|
100
|
+
|
|
101
|
+
.. code-block:: python
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
"network": {"host": "localhost", "ports": [9000]},
|
|
105
|
+
"enabled": False,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
See :doc:`storage` for the difference between in-memory defaults and persisted
|
|
109
|
+
updates.
|
upref-2.2.0/docs/conf.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Sphinx configuration for the Upref documentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
9
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
10
|
+
|
|
11
|
+
import upref # noqa: E402 (the source tree must be importable first)
|
|
12
|
+
|
|
13
|
+
# Project information
|
|
14
|
+
project = "upref"
|
|
15
|
+
author = upref.__author__
|
|
16
|
+
copyright = f"2026, {author}"
|
|
17
|
+
release = upref.__version__
|
|
18
|
+
version = ".".join(release.split(".")[:2])
|
|
19
|
+
|
|
20
|
+
# General configuration
|
|
21
|
+
extensions = [
|
|
22
|
+
"sphinx.ext.autodoc",
|
|
23
|
+
"sphinx.ext.autosummary",
|
|
24
|
+
"sphinx.ext.napoleon",
|
|
25
|
+
"myst_parser",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
autosummary_generate = True
|
|
29
|
+
autodoc_member_order = "bysource"
|
|
30
|
+
autodoc_typehints = "description"
|
|
31
|
+
nitpicky = True
|
|
32
|
+
nitpick_ignore_regex = [
|
|
33
|
+
("py:class", r"(collections\.abc\.)?(Callable|Mapping)"),
|
|
34
|
+
("py:class", r"(os\.)?PathLike"),
|
|
35
|
+
("py:class", r"pathlib\.Path"),
|
|
36
|
+
("py:class", r"(PrintFunction|ReadFunction)"),
|
|
37
|
+
("py:func", r"(getpass\.getpass|input|print)"),
|
|
38
|
+
("py:mod", r"platformdirs"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
source_suffix = {
|
|
42
|
+
".rst": "restructuredtext",
|
|
43
|
+
".md": "markdown",
|
|
44
|
+
}
|
|
45
|
+
root_doc = "index"
|
|
46
|
+
language = "en"
|
|
47
|
+
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
|
48
|
+
|
|
49
|
+
# HTML output
|
|
50
|
+
html_theme = "sphinx_rtd_theme"
|
|
51
|
+
html_static_path = ["layout"]
|
|
52
|
+
html_css_files = ["extra.css"]
|