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.
Files changed (100) hide show
  1. upref-2.2.0/.gitattributes +3 -0
  2. upref-2.2.0/CHANGELOG.md +92 -0
  3. upref-2.2.0/MANIFEST.in +7 -0
  4. {upref-2.0.1 → upref-2.2.0}/PKG-INFO +77 -14
  5. {upref-2.0.1 → upref-2.2.0}/README.md +74 -13
  6. upref-2.2.0/docs/api.rst +110 -0
  7. upref-2.2.0/docs/changelog.md +2 -0
  8. upref-2.2.0/docs/concepts.rst +109 -0
  9. upref-2.2.0/docs/conf.py +52 -0
  10. upref-2.2.0/docs/development.rst +163 -0
  11. upref-2.2.0/docs/errors.rst +111 -0
  12. upref-2.2.0/docs/examples.rst +188 -0
  13. upref-2.2.0/docs/index.rst +57 -0
  14. upref-2.2.0/docs/layout/extra.css +4 -0
  15. upref-2.2.0/docs/layout/tower.png +0 -0
  16. upref-2.2.0/docs/legacy_api.rst +57 -0
  17. upref-2.2.0/docs/license_link.md +4 -0
  18. upref-2.2.0/docs/migration.rst +162 -0
  19. upref-2.2.0/docs/paths.rst +114 -0
  20. upref-2.2.0/docs/prompting.rst +250 -0
  21. upref-2.2.0/docs/readme_link.md +5 -0
  22. upref-2.2.0/docs/recipes.rst +106 -0
  23. upref-2.2.0/docs/releasing.rst +92 -0
  24. upref-2.2.0/docs/review.rst +135 -0
  25. upref-2.2.0/docs/security.rst +106 -0
  26. upref-2.2.0/docs/storage.rst +173 -0
  27. upref-2.2.0/docs/troubleshooting.rst +87 -0
  28. upref-2.2.0/docs/user_preferences.rst +172 -0
  29. upref-2.2.0/examples/README.md +97 -0
  30. upref-2.2.0/examples/account_preferences.py +50 -0
  31. upref-2.2.0/examples/apply_preferences.py +57 -0
  32. upref-2.2.0/examples/backup_restore.py +35 -0
  33. upref-2.2.0/examples/basic_store.py +16 -0
  34. upref-2.2.0/examples/boolean_collection.py +19 -0
  35. upref-2.2.0/examples/custom_interface.py +40 -0
  36. upref-2.2.0/examples/defaults_and_update.py +30 -0
  37. upref-2.2.0/examples/edit_settings.py +38 -0
  38. upref-2.2.0/examples/environment_profiles.py +85 -0
  39. upref-2.2.0/examples/first_run_preferences.py +69 -0
  40. upref-2.2.0/examples/gui_collection.py +39 -0
  41. upref-2.2.0/examples/handle_errors.py +26 -0
  42. upref-2.2.0/examples/import_export_preferences.py +89 -0
  43. upref-2.2.0/examples/migrate_v1.py +18 -0
  44. upref-2.2.0/examples/migration_preview.py +36 -0
  45. upref-2.2.0/examples/multiple_projects.py +32 -0
  46. upref-2.2.0/examples/nested_collection.py +49 -0
  47. upref-2.2.0/examples/portable_store.py +23 -0
  48. upref-2.2.0/examples/project_variables.py +53 -0
  49. upref-2.2.0/examples/recent_files.py +44 -0
  50. upref-2.2.0/examples/reset_preferences.py +46 -0
  51. upref-2.2.0/examples/schema_upgrade.py +45 -0
  52. upref-2.2.0/examples/session_overrides.py +59 -0
  53. upref-2.2.0/examples/tty_collection.py +29 -0
  54. upref-2.2.0/examples/typed_settings.py +42 -0
  55. upref-2.2.0/examples/window_preferences.py +44 -0
  56. upref-2.2.0/make.bat +140 -0
  57. {upref-2.0.1 → upref-2.2.0}/pyproject.toml +5 -1
  58. upref-2.2.0/pytest.ini +45 -0
  59. upref-2.2.0/scripts/add_license_headers.py +520 -0
  60. upref-2.2.0/scripts/bootstrap.ps1 +210 -0
  61. upref-2.2.0/scripts/check_installed_package.py +41 -0
  62. upref-2.2.0/tests/integration/_gui_scenario.py +165 -0
  63. upref-2.2.0/tests/integration/test_gui.py +29 -0
  64. {upref-2.0.1 → upref-2.2.0}/tests/test_core.py +12 -2
  65. upref-2.2.0/tests/test_examples.py +147 -0
  66. {upref-2.0.1 → upref-2.2.0}/tests/test_legacy.py +102 -0
  67. {upref-2.0.1 → upref-2.2.0}/tests/test_paths.py +20 -0
  68. upref-2.2.0/tests/test_preference_examples.py +210 -0
  69. {upref-2.0.1 → upref-2.2.0}/tests/test_prompt.py +82 -3
  70. upref-2.2.0/tests/test_properties.py +92 -0
  71. {upref-2.0.1 → upref-2.2.0}/tests/test_public_api.py +2 -1
  72. {upref-2.0.1 → upref-2.2.0}/tests/test_storage.py +142 -6
  73. {upref-2.0.1 → upref-2.2.0}/tests/test_tty.py +65 -0
  74. {upref-2.0.1 → upref-2.2.0}/tests/test_types.py +54 -0
  75. {upref-2.0.1 → upref-2.2.0}/upref/__init__.py +3 -2
  76. {upref-2.0.1 → upref-2.2.0}/upref/_paths.py +4 -1
  77. {upref-2.0.1 → upref-2.2.0}/upref/_storage.py +93 -29
  78. {upref-2.0.1 → upref-2.2.0}/upref/_types.py +18 -7
  79. {upref-2.0.1 → upref-2.2.0}/upref/core.py +26 -8
  80. {upref-2.0.1 → upref-2.2.0}/upref/gui.py +5 -4
  81. {upref-2.0.1 → upref-2.2.0}/upref/legacy.py +26 -8
  82. {upref-2.0.1 → upref-2.2.0}/upref/prompt.py +54 -4
  83. {upref-2.0.1 → upref-2.2.0}/upref/tty.py +25 -3
  84. {upref-2.0.1 → upref-2.2.0}/upref.egg-info/PKG-INFO +77 -14
  85. upref-2.2.0/upref.egg-info/SOURCES.txt +97 -0
  86. {upref-2.0.1 → upref-2.2.0}/upref.egg-info/requires.txt +2 -0
  87. upref-2.0.1/upref.egg-info/SOURCES.txt +0 -34
  88. {upref-2.0.1 → upref-2.2.0}/LICENSE.md +0 -0
  89. {upref-2.0.1 → upref-2.2.0}/setup.cfg +0 -0
  90. {upref-2.0.1 → upref-2.2.0}/setup.py +0 -0
  91. {upref-2.0.1 → upref-2.2.0}/tests/test_documentation.py +0 -0
  92. {upref-2.0.1 → upref-2.2.0}/tests/test_license_headers.py +0 -0
  93. {upref-2.0.1 → upref-2.2.0}/tests/test_merge.py +0 -0
  94. {upref-2.0.1 → upref-2.2.0}/upref/_merge.py +0 -0
  95. {upref-2.0.1 → upref-2.2.0}/upref/errors.py +0 -0
  96. {upref-2.0.1 → upref-2.2.0}/upref/py.typed +0 -0
  97. {upref-2.0.1 → upref-2.2.0}/upref/resources/__init__.py +0 -0
  98. {upref-2.0.1 → upref-2.2.0}/upref/resources/tower.ico +0 -0
  99. {upref-2.0.1 → upref-2.2.0}/upref.egg-info/dependency_links.txt +0 -0
  100. {upref-2.0.1 → upref-2.2.0}/upref.egg-info/top_level.txt +0 -0
@@ -0,0 +1,3 @@
1
+ *.bat text eol=crlf
2
+ *.cmd text eol=crlf
3
+ *.py text eol=lf
@@ -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.
@@ -0,0 +1,7 @@
1
+ include pytest.ini make.bat .gitattributes
2
+ include CHANGELOG.md
3
+ recursive-include examples *.py *.md
4
+ recursive-include scripts *.py *.ps1
5
+ recursive-include tests *.py
6
+ recursive-include docs *.rst *.md *.py *.css *.png
7
+ prune docs/_build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upref
3
- Version: 2.0.1
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/). Runnable examples live in
237
- [`examples`](https://github.com/IIXIXII/upref/tree/master/examples):
238
-
239
- - `basic_store.py` covers a save/load cycle;
240
- - `defaults_and_update.py` covers defaults, recursive updates, and deletion;
241
- - `project_variables.py` collects required variables for one project;
242
- - `environment_profiles.py` selects profiles with temporary environment overrides;
243
- - `multiple_projects.py` stores independent settings for related projects;
244
- - `tty_collection.py` collects missing values without a GUI;
245
- - `migrate_v1.py` imports a historical configuration.
246
-
247
- Except for the migration example, these programs use the platform's per-user
248
- configuration directory. They do not create files in the repository.
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/). Runnable examples live in
187
- [`examples`](https://github.com/IIXIXII/upref/tree/master/examples):
188
-
189
- - `basic_store.py` covers a save/load cycle;
190
- - `defaults_and_update.py` covers defaults, recursive updates, and deletion;
191
- - `project_variables.py` collects required variables for one project;
192
- - `environment_profiles.py` selects profiles with temporary environment overrides;
193
- - `multiple_projects.py` stores independent settings for related projects;
194
- - `tty_collection.py` collects missing values without a GUI;
195
- - `migrate_v1.py` imports a historical configuration.
196
-
197
- Except for the migration example, these programs use the platform's per-user
198
- configuration directory. They do not create files in the repository.
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
 
@@ -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,2 @@
1
+ ```{include} ../CHANGELOG.md
2
+ ```
@@ -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.
@@ -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"]