upref 2.1.0__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 (99) hide show
  1. {upref-2.1.0 → upref-2.2.0}/CHANGELOG.md +42 -0
  2. {upref-2.1.0 → upref-2.2.0}/MANIFEST.in +1 -0
  3. {upref-2.1.0 → upref-2.2.0}/PKG-INFO +21 -2
  4. {upref-2.1.0 → upref-2.2.0}/README.md +18 -1
  5. {upref-2.1.0 → upref-2.2.0}/docs/concepts.rst +6 -2
  6. {upref-2.1.0 → upref-2.2.0}/docs/development.rst +32 -5
  7. {upref-2.1.0 → upref-2.2.0}/docs/examples.rst +39 -5
  8. {upref-2.1.0 → upref-2.2.0}/docs/index.rst +1 -0
  9. {upref-2.1.0 → upref-2.2.0}/docs/migration.rst +46 -3
  10. {upref-2.1.0 → upref-2.2.0}/docs/recipes.rst +4 -0
  11. {upref-2.1.0 → upref-2.2.0}/docs/releasing.rst +3 -3
  12. {upref-2.1.0 → upref-2.2.0}/docs/review.rst +25 -0
  13. {upref-2.1.0 → upref-2.2.0}/docs/security.rst +16 -0
  14. {upref-2.1.0 → upref-2.2.0}/docs/storage.rst +4 -0
  15. upref-2.2.0/docs/user_preferences.rst +172 -0
  16. {upref-2.1.0 → upref-2.2.0}/examples/README.md +42 -0
  17. upref-2.2.0/examples/account_preferences.py +50 -0
  18. upref-2.2.0/examples/apply_preferences.py +57 -0
  19. upref-2.2.0/examples/backup_restore.py +35 -0
  20. upref-2.2.0/examples/first_run_preferences.py +69 -0
  21. upref-2.2.0/examples/import_export_preferences.py +89 -0
  22. {upref-2.1.0 → upref-2.2.0}/examples/migrate_v1.py +4 -0
  23. upref-2.2.0/examples/migration_preview.py +36 -0
  24. upref-2.2.0/examples/recent_files.py +44 -0
  25. upref-2.2.0/examples/reset_preferences.py +46 -0
  26. upref-2.2.0/examples/session_overrides.py +59 -0
  27. upref-2.2.0/examples/window_preferences.py +44 -0
  28. {upref-2.1.0 → upref-2.2.0}/pyproject.toml +4 -1
  29. {upref-2.1.0 → upref-2.2.0}/pytest.ini +4 -0
  30. upref-2.2.0/scripts/check_installed_package.py +41 -0
  31. upref-2.2.0/tests/integration/_gui_scenario.py +165 -0
  32. upref-2.2.0/tests/integration/test_gui.py +29 -0
  33. {upref-2.1.0 → upref-2.2.0}/tests/test_examples.py +30 -1
  34. {upref-2.1.0 → upref-2.2.0}/tests/test_legacy.py +102 -0
  35. {upref-2.1.0 → upref-2.2.0}/tests/test_paths.py +20 -0
  36. upref-2.2.0/tests/test_preference_examples.py +210 -0
  37. {upref-2.1.0 → upref-2.2.0}/tests/test_prompt.py +23 -1
  38. upref-2.2.0/tests/test_properties.py +92 -0
  39. {upref-2.1.0 → upref-2.2.0}/tests/test_public_api.py +1 -1
  40. {upref-2.1.0 → upref-2.2.0}/tests/test_storage.py +75 -6
  41. {upref-2.1.0 → upref-2.2.0}/tests/test_types.py +46 -0
  42. {upref-2.1.0 → upref-2.2.0}/upref/__init__.py +1 -1
  43. {upref-2.1.0 → upref-2.2.0}/upref/_paths.py +4 -1
  44. {upref-2.1.0 → upref-2.2.0}/upref/_storage.py +48 -28
  45. {upref-2.1.0 → upref-2.2.0}/upref/_types.py +14 -6
  46. {upref-2.1.0 → upref-2.2.0}/upref/core.py +20 -6
  47. {upref-2.1.0 → upref-2.2.0}/upref/legacy.py +26 -8
  48. {upref-2.1.0 → upref-2.2.0}/upref.egg-info/PKG-INFO +21 -2
  49. {upref-2.1.0 → upref-2.2.0}/upref.egg-info/SOURCES.txt +16 -0
  50. {upref-2.1.0 → upref-2.2.0}/upref.egg-info/requires.txt +2 -0
  51. {upref-2.1.0 → upref-2.2.0}/.gitattributes +0 -0
  52. {upref-2.1.0 → upref-2.2.0}/LICENSE.md +0 -0
  53. {upref-2.1.0 → upref-2.2.0}/docs/api.rst +0 -0
  54. {upref-2.1.0 → upref-2.2.0}/docs/changelog.md +0 -0
  55. {upref-2.1.0 → upref-2.2.0}/docs/conf.py +0 -0
  56. {upref-2.1.0 → upref-2.2.0}/docs/errors.rst +0 -0
  57. {upref-2.1.0 → upref-2.2.0}/docs/layout/extra.css +0 -0
  58. {upref-2.1.0 → upref-2.2.0}/docs/layout/tower.png +0 -0
  59. {upref-2.1.0 → upref-2.2.0}/docs/legacy_api.rst +0 -0
  60. {upref-2.1.0 → upref-2.2.0}/docs/license_link.md +0 -0
  61. {upref-2.1.0 → upref-2.2.0}/docs/paths.rst +0 -0
  62. {upref-2.1.0 → upref-2.2.0}/docs/prompting.rst +0 -0
  63. {upref-2.1.0 → upref-2.2.0}/docs/readme_link.md +0 -0
  64. {upref-2.1.0 → upref-2.2.0}/docs/troubleshooting.rst +0 -0
  65. {upref-2.1.0 → upref-2.2.0}/examples/basic_store.py +0 -0
  66. {upref-2.1.0 → upref-2.2.0}/examples/boolean_collection.py +0 -0
  67. {upref-2.1.0 → upref-2.2.0}/examples/custom_interface.py +0 -0
  68. {upref-2.1.0 → upref-2.2.0}/examples/defaults_and_update.py +0 -0
  69. {upref-2.1.0 → upref-2.2.0}/examples/edit_settings.py +0 -0
  70. {upref-2.1.0 → upref-2.2.0}/examples/environment_profiles.py +0 -0
  71. {upref-2.1.0 → upref-2.2.0}/examples/gui_collection.py +0 -0
  72. {upref-2.1.0 → upref-2.2.0}/examples/handle_errors.py +0 -0
  73. {upref-2.1.0 → upref-2.2.0}/examples/multiple_projects.py +0 -0
  74. {upref-2.1.0 → upref-2.2.0}/examples/nested_collection.py +0 -0
  75. {upref-2.1.0 → upref-2.2.0}/examples/portable_store.py +0 -0
  76. {upref-2.1.0 → upref-2.2.0}/examples/project_variables.py +0 -0
  77. {upref-2.1.0 → upref-2.2.0}/examples/schema_upgrade.py +0 -0
  78. {upref-2.1.0 → upref-2.2.0}/examples/tty_collection.py +0 -0
  79. {upref-2.1.0 → upref-2.2.0}/examples/typed_settings.py +0 -0
  80. {upref-2.1.0 → upref-2.2.0}/make.bat +0 -0
  81. {upref-2.1.0 → upref-2.2.0}/scripts/add_license_headers.py +0 -0
  82. {upref-2.1.0 → upref-2.2.0}/scripts/bootstrap.ps1 +0 -0
  83. {upref-2.1.0 → upref-2.2.0}/setup.cfg +0 -0
  84. {upref-2.1.0 → upref-2.2.0}/setup.py +0 -0
  85. {upref-2.1.0 → upref-2.2.0}/tests/test_core.py +0 -0
  86. {upref-2.1.0 → upref-2.2.0}/tests/test_documentation.py +0 -0
  87. {upref-2.1.0 → upref-2.2.0}/tests/test_license_headers.py +0 -0
  88. {upref-2.1.0 → upref-2.2.0}/tests/test_merge.py +0 -0
  89. {upref-2.1.0 → upref-2.2.0}/tests/test_tty.py +0 -0
  90. {upref-2.1.0 → upref-2.2.0}/upref/_merge.py +0 -0
  91. {upref-2.1.0 → upref-2.2.0}/upref/errors.py +0 -0
  92. {upref-2.1.0 → upref-2.2.0}/upref/gui.py +0 -0
  93. {upref-2.1.0 → upref-2.2.0}/upref/prompt.py +0 -0
  94. {upref-2.1.0 → upref-2.2.0}/upref/py.typed +0 -0
  95. {upref-2.1.0 → upref-2.2.0}/upref/resources/__init__.py +0 -0
  96. {upref-2.1.0 → upref-2.2.0}/upref/resources/tower.ico +0 -0
  97. {upref-2.1.0 → upref-2.2.0}/upref/tty.py +0 -0
  98. {upref-2.1.0 → upref-2.2.0}/upref.egg-info/dependency_links.txt +0 -0
  99. {upref-2.1.0 → upref-2.2.0}/upref.egg-info/top_level.txt +0 -0
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
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
+
3
45
  ## 2.1.0
4
46
 
5
47
  ### Added
@@ -2,5 +2,6 @@ include pytest.ini make.bat .gitattributes
2
2
  include CHANGELOG.md
3
3
  recursive-include examples *.py *.md
4
4
  recursive-include scripts *.py *.ps1
5
+ recursive-include tests *.py
5
6
  recursive-include docs *.rst *.md *.py *.css *.png
6
7
  prune docs/_build
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: upref
3
- Version: 2.1.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"
@@ -245,10 +247,18 @@ store = ConfigStore("my-application")
245
247
  config = store.import_legacy("my_personnal_data")
246
248
  ```
247
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
+
248
256
  The source file is left untouched, and using it as the v2 target is rejected.
249
257
  Migration checks for an existing v2 file unless `overwrite=True` is passed;
250
258
  because this preflight check is not locked, applications with concurrent
251
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.
252
262
 
253
263
  ## Security
254
264
 
@@ -263,13 +273,18 @@ reference in Upref.
263
273
 
264
274
  The complete user guide and API reference are available on
265
275
  [Read the Docs](https://upref.readthedocs.io/). The
266
- [example catalog](docs/examples.rst) lists 16 runnable programs by difficulty,
276
+ [example catalog](docs/examples.rst) lists 26 runnable programs by difficulty,
267
277
  input method, and file effects. Start with these:
268
278
 
269
279
  | Level | Example | What it demonstrates |
270
280
  | --- | --- | --- |
271
281
  | Simple | `portable_store.py` | Defaults, save/load, and updates in a temporary directory |
272
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 |
273
288
  | Intermediate | `edit_settings.py` | Enter to keep values and editable JSON lists |
274
289
  | Intermediate | `gui_collection.py` | GUI ownership, formatted prefill, and cancellation |
275
290
  | Intermediate | `handle_errors.py` | Reporting malformed YAML while retaining the file |
@@ -288,6 +303,10 @@ python examples/portable_store.py
288
303
  python examples/custom_interface.py
289
304
  ```
290
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
+
291
310
  See also the [troubleshooting guide](docs/troubleshooting.rst) and the
292
311
  [code review and compatibility notes](docs/review.rst).
293
312
 
@@ -195,10 +195,18 @@ store = ConfigStore("my-application")
195
195
  config = store.import_legacy("my_personnal_data")
196
196
  ```
197
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
+
198
204
  The source file is left untouched, and using it as the v2 target is rejected.
199
205
  Migration checks for an existing v2 file unless `overwrite=True` is passed;
200
206
  because this preflight check is not locked, applications with concurrent
201
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.
202
210
 
203
211
  ## Security
204
212
 
@@ -213,13 +221,18 @@ reference in Upref.
213
221
 
214
222
  The complete user guide and API reference are available on
215
223
  [Read the Docs](https://upref.readthedocs.io/). The
216
- [example catalog](docs/examples.rst) lists 16 runnable programs by difficulty,
224
+ [example catalog](docs/examples.rst) lists 26 runnable programs by difficulty,
217
225
  input method, and file effects. Start with these:
218
226
 
219
227
  | Level | Example | What it demonstrates |
220
228
  | --- | --- | --- |
221
229
  | Simple | `portable_store.py` | Defaults, save/load, and updates in a temporary directory |
222
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 |
223
236
  | Intermediate | `edit_settings.py` | Enter to keep values and editable JSON lists |
224
237
  | Intermediate | `gui_collection.py` | GUI ownership, formatted prefill, and cancellation |
225
238
  | Intermediate | `handle_errors.py` | Reporting malformed YAML while retaining the file |
@@ -238,6 +251,10 @@ python examples/portable_store.py
238
251
  python examples/custom_interface.py
239
252
  ```
240
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
+
241
258
  See also the [troubleshooting guide](docs/troubleshooting.rst) and the
242
259
  [code review and compatibility notes](docs/review.rst).
243
260
 
@@ -45,9 +45,13 @@ of the following:
45
45
  Empty strings, empty containers, ``False``, and ``0`` are valid values. An
46
46
  empty document or explicit YAML ``null`` at the root represents an empty
47
47
  mapping. A list or non-null scalar at the document root is invalid. Mapping
48
- keys at every level must be strings.
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.
49
53
 
50
- Objects such as ``pathlib.Path``, ``datetime``, tuples, sets, enum
54
+ Values such as ``pathlib.Path``, ``datetime``, tuples, sets, enum
51
55
  members, and application classes are not converted implicitly. Convert them
52
56
  to a supported representation before calling
53
57
  :meth:`~upref.ConfigStore.save`.
@@ -82,19 +82,46 @@ Equivalent tools can be run directly through the environment interpreter:
82
82
  .\.venv\Scripts\python.exe -m mypy upref examples
83
83
  .\.venv\Scripts\python.exe -m sphinx -E -a -W --keep-going -b html docs docs\_build\html
84
84
 
85
- Continuous integration also measures statement coverage and requires 100
86
- percent. Reproduce that gate locally with:
85
+ Continuous integration measures statement and branch coverage and requires
86
+ 100 percent. Reproduce that gate locally with:
87
87
 
88
88
  .. code-block:: console
89
89
 
90
90
  .\.venv\Scripts\python.exe -m pytest --cov=upref --cov-report=term-missing
91
91
 
92
- Behavioral checks matter in addition to statement coverage. ``test_examples.py``
92
+ Behavioral checks matter in addition to coverage. ``test_properties.py`` uses
93
+ Hypothesis to generate nested configurations and verify YAML round trips,
94
+ independence of mutable containers, merge identities, and preservation of the
95
+ original file after a failed replacement. NaN is excluded from equality-based
96
+ properties because it does not compare equal to itself.
97
+
98
+ ``test_examples.py``
93
99
  runs storage and advanced recipes in isolated directories, supplies deterministic
94
100
  terminal answers, and verifies cancellation without saving. Tests for GUI
95
101
  arguments and lifecycle use a wxPython substitute and do not require a display.
96
- After changing GUI presentation, also run ``examples/gui_collection.py`` in a
97
- desktop session with the GUI extra installed to check actual rendering.
102
+ Native GUI integration tests run in a dedicated Windows CI job. Each scenario
103
+ uses a separate process with a 40-second timeout and drives real modal dialogs
104
+ through wx's event loop. They check app ownership, repeated dialogs, formatted
105
+ prefill, cancellation, password controls, and cleanup after an injected error.
106
+ They are skipped in the ordinary suite. Run them in a desktop session with:
107
+
108
+ .. code-block:: powershell
109
+
110
+ .\.venv\Scripts\python.exe -m pip install -e '.[test,gui]'
111
+ $env:UPREF_RUN_GUI_TESTS = '1'
112
+ .\.venv\Scripts\python.exe -m pytest tests/integration -q --no-cov
113
+ Remove-Item Env:UPREF_RUN_GUI_TESTS
114
+
115
+ The dedicated run disables coverage because the complete coverage gate belongs
116
+ to the ordinary unit suite. Missing wxPython is an error when native tests are
117
+ explicitly enabled. Visual layout can additionally be inspected with
118
+ ``examples/gui_collection.py``.
119
+
120
+ CI also runs the ordinary suite on Python 3.10 with ``platformdirs==4.0.0`` and
121
+ ``PyYAML==6.0`` to check the advertised minimum runtime dependencies. The wheel
122
+ job and release workflow invoke ``scripts/check_installed_package.py`` using
123
+ Python's isolated mode in a clean environment: it checks packaged resources
124
+ and the save/load/update/delete cycle without importing the checkout.
98
125
 
99
126
  Documentation workflow
100
127
  ----------------------
@@ -3,11 +3,12 @@
3
3
  Examples
4
4
  ========
5
5
 
6
- The 16 programs in ``examples`` are complete and runnable from a checkout with
7
- the project installed in ``.venv``. The nine recipes listed first use memory
8
- or automatically cleaned temporary directories. The seven persistent examples
9
- below use distinct per-user configuration locations; migration reads an
10
- existing historical file and creates a separate v2 file.
6
+ The 26 programs in ``examples`` are complete and runnable from a checkout with
7
+ the project installed in ``.venv``. Ten user-preference scenarios and nine
8
+ fundamental recipes use memory or automatically cleaned temporary directories.
9
+ The seven persistent examples below use distinct per-user configuration
10
+ locations; migration reads an existing historical file and creates a separate
11
+ v2 file.
11
12
 
12
13
  Run an example on Windows with:
13
14
 
@@ -15,6 +16,39 @@ Run an example on Windows with:
15
16
 
16
17
  .\.venv\Scripts\python.exe examples\basic_store.py
17
18
 
19
+ User preference scenarios
20
+ --------------------------
21
+
22
+ Start with :doc:`user_preferences` for everyday application behavior. All ten
23
+ scenarios use temporary files, and only the first two require terminal input.
24
+
25
+ .. list-table::
26
+ :header-rows: 1
27
+ :widths: 35 65
28
+
29
+ * - Script
30
+ - User scenario
31
+ * - ``first_run_preferences.py``
32
+ - Initial setup and reuse on the next startup.
33
+ * - ``apply_preferences.py``
34
+ - Edit a draft, then confirm, discard, or cancel.
35
+ * - ``reset_preferences.py``
36
+ - Remove one override, a section, or the whole configuration.
37
+ * - ``recent_files.py``
38
+ - Keep a bounded history of recently opened documents.
39
+ * - ``window_preferences.py``
40
+ - Restore visible window geometry after changing displays.
41
+ * - ``account_preferences.py``
42
+ - Keep separate preferences for accounts within one OS user.
43
+ * - ``session_overrides.py``
44
+ - Override preferences through CLI options with opt-in persistence.
45
+ * - ``import_export_preferences.py``
46
+ - Transfer validated portable preferences without local history.
47
+ * - ``backup_restore.py``
48
+ - Restore a previous preference snapshot.
49
+ * - ``migration_preview.py``
50
+ - Preview legacy conversion and preserve ambiguous raw data.
51
+
18
52
  Progressive recipes
19
53
  -------------------
20
54
 
@@ -17,6 +17,7 @@ task-oriented guides below for behavioral details and the :doc:`API reference
17
17
  readme_link
18
18
  concepts
19
19
  examples
20
+ user_preferences
20
21
  recipes
21
22
 
22
23
  .. toctree::
@@ -60,15 +60,45 @@ Migration behavior
60
60
  * recognized descriptor mappings are converted to raw values and metadata
61
61
  entries and descriptors without a ``value`` member are omitted;
62
62
  * the destination uses the same validation and atomic save as every v2 store;
63
- * the returned mapping is the mapping written to the destination.
63
+ * the returned mapping is the mapping written to the destination, or the
64
+ proposed conversion when ``dry_run=True``.
64
65
 
65
66
  Descriptor detection is necessarily structural because v1 files carry no
66
67
  format version. Upref treats a document as descriptors when every
67
68
  non-metadata top-level value is a mapping containing at least one of
68
69
  ``label``, ``description``, ``type``, or ``value``, and at least one field has
69
70
  a ``value`` member. A raw configuration with that same shape is ambiguous and
70
- may be converted. For such data, load and transform it explicitly before
71
- calling :meth:`~upref.ConfigStore.save`.
71
+ may be converted. Use ``source_format="raw"`` to preserve every key of such
72
+ data. Use ``source_format="descriptors"`` to force extraction of ``value``
73
+ entries, dropping metadata and descriptors without a value. The default,
74
+ ``source_format="auto"``, retains the historical detection behavior.
75
+
76
+ For example, ``{"timeout": {"value": 30, "unit": "seconds"}}`` becomes
77
+ ``{"timeout": 30}`` in auto or descriptor mode. Raw mode preserves the full
78
+ mapping, including ``unit``.
79
+
80
+ Previewing a conversion
81
+ ~~~~~~~~~~~~~~~~~~~~~~~~
82
+
83
+ Inspect the conversion without writing or creating the destination directory:
84
+
85
+ .. code-block:: python
86
+
87
+ preview = store.import_legacy(
88
+ "my_personnal_data", source_format="raw", dry_run=True
89
+ )
90
+ # Inspect or validate preview before choosing to perform the import.
91
+ migrated = store.import_legacy("my_personnal_data", source_format="raw")
92
+
93
+ A dry run still checks that the source exists, source and target differ, and
94
+ the target does not already exist unless ``overwrite=True``. It does not test
95
+ write permissions or serialization at the destination. Previewing an existing
96
+ target with ``overwrite=True, dry_run=True`` leaves that target unchanged.
97
+ The returned tree is detached. A later import reads the source again; a preview
98
+ neither reserves the target nor freezes the source against other writers.
99
+
100
+ Overwriting a target
101
+ ~~~~~~~~~~~~~~~~~~~~~
72
102
 
73
103
  Use ``overwrite=True`` only after deciding that the legacy file is the source
74
104
  of truth:
@@ -117,3 +147,16 @@ replacements:
117
147
  The compatibility layer preserves v1 locations and descriptor shape; it is
118
148
  not the recommended way to create a new v2 file. See :doc:`legacy_api` for its
119
149
  complete reference.
150
+
151
+ Removal schedule
152
+ ~~~~~~~~~~~~~~~~
153
+
154
+ The compatibility wrappers remain supported throughout all 2.x releases.
155
+ Their removal is scheduled for 3.0, and their warnings name that boundary.
156
+ There is no calendar release date for 3.0. Migrate callers and run application
157
+ tests with deprecation warnings enabled before adopting that major version.
158
+
159
+ This schedule covers the deprecated functions listed above, both at package
160
+ level and in ``upref.legacy``. Explicit file migration through
161
+ ``ConfigStore.import_legacy`` is retained; removing the wrappers will not
162
+ require users to abandon old files before they can upgrade.
@@ -6,6 +6,10 @@ them from an installed checkout with ``python examples/<name>.py``. All files
6
6
  created by the recipes below use automatically cleaned temporary directories;
7
7
  the interactive collection examples otherwise work entirely in memory.
8
8
 
9
+ For first-run setup, Apply/Cancel, resets, recent documents, window geometry,
10
+ account preferences, session overrides, and preference transfer, see the ten
11
+ additional walkthroughs in :doc:`user_preferences`.
12
+
9
13
  A first store without persistent files
10
14
  --------------------------------------
11
15
 
@@ -38,7 +38,7 @@ Preparing a release
38
38
  -------------------
39
39
 
40
40
  Release tags use a ``v`` prefix while Python package metadata does not. For
41
- example, package version ``2.1.0`` is released with tag ``v2.1.0``.
41
+ example, package version ``2.2.0`` is released with tag ``v2.2.0``.
42
42
 
43
43
  #. Update ``project.version`` in ``pyproject.toml`` and the source-tree fallback
44
44
  ``upref.__version__`` in ``upref/__init__.py`` to the same PEP 440 version.
@@ -67,8 +67,8 @@ example, package version ``2.1.0`` is released with tag ``v2.1.0``.
67
67
 
68
68
  .. code-block:: console
69
69
 
70
- git tag -a v2.1.0 -m "Release v2.1.0"
71
- git push origin v2.1.0
70
+ git tag -a v2.2.0 -m "Release v2.2.0"
71
+ git push origin v2.2.0
72
72
 
73
73
  The release workflow rejects a tag that does not match ``project.version``,
74
74
  does not identify the checked-out commit, or does not belong to ``master``.
@@ -108,3 +108,28 @@ heuristic can be ambiguous for raw mappings that resemble descriptors. These
108
108
  constraints are described in :doc:`security`, :doc:`storage`, and
109
109
  :doc:`migration`; addressing them requires separate API and compatibility
110
110
  decisions.
111
+
112
+ Follow-up hardening
113
+ -------------------
114
+
115
+ The subsequent implementation adds explicit migration format selection and
116
+ dry-run previews while preserving the default heuristic. Invalid scalar-tagged
117
+ YAML keys now report format errors, null characters in directories are rejected
118
+ at construction, and string subclass keys are normalized to ordinary strings.
119
+
120
+ Generated round-trip tests found that PyYAML folds Unicode NEL (U+0085) into a
121
+ space when emitted literally. Upref's safe dumper now escapes that character
122
+ in keys and values while retaining readable ordinary Unicode. Temporary-file
123
+ cleanup is registered when the file is created and runs after the stream closes,
124
+ including on write, flush, permission, and replacement failures.
125
+
126
+ The local Windows/Python 3.14 suite passes 267 tests with 100% statement and
127
+ branch coverage. Three platform-dependent checks and the two opt-in native GUI
128
+ scenarios are skipped by the ordinary command. The two native scenarios were
129
+ also run separately with wxPython 4.3.1 and passed, exercising both owned and
130
+ borrowed applications with real dialogs.
131
+
132
+ CI now includes native GUI and minimum-runtime-dependency jobs. CI and release
133
+ automation check installed-wheel resources and persistence in isolated mode.
134
+ The 2.x/3.0 wrapper removal schedule is documented in :doc:`migration`;
135
+ criteria for adding locking or resource budgets are in :doc:`security`.
@@ -88,3 +88,19 @@ Applications using Upref should also:
88
88
  * treat configuration imported from another machine as untrusted input;
89
89
  * keep backups and crash reports from collecting secrets accidentally;
90
90
  * add cross-process locking when lost updates are unacceptable.
91
+
92
+ Criteria for expanding the storage contract
93
+ -------------------------------------------
94
+
95
+ Upref's current scope remains small, local configuration files. If an
96
+ application allows several processes to write the same file and cannot accept
97
+ lost updates, coordinate the complete read/merge/write operation with an
98
+ external lock. A future built-in transaction API would need portable locking,
99
+ timeouts, failure recovery, and multiprocess tests before becoming a guarantee.
100
+
101
+ If configurations will be accepted from external parties, input byte limits
102
+ alone are insufficient: aliases can expand during normalization. A bounded
103
+ loader would need explicit limits for depth and expanded nodes as well as
104
+ input size. Such limits should be opt-in initially to avoid silently changing
105
+ the accepted configuration model. They are not implemented by this release;
106
+ use the isolation and validation measures described above for those inputs.
@@ -92,6 +92,10 @@ kept in insertion order. YAML comments, anchors, aliases, quoting choices,
92
92
  and custom formatting are not part of the configuration model and are not
93
93
  preserved on save.
94
94
 
95
+ Unicode NEL (U+0085) is escaped in quoted strings to prevent YAML line folding
96
+ from changing it into a space. This applies to both keys and values; ordinary
97
+ Unicode text remains readable.
98
+
95
99
  Applying selected changes
96
100
  -------------------------
97
101
 
@@ -0,0 +1,172 @@
1
+ User preference scenarios
2
+ ==========================
3
+
4
+ These ten programs cover the lifecycle of preferences in a desktop or terminal
5
+ application. They use only Upref and the Python standard library. Run them
6
+ from an installed checkout, for example:
7
+
8
+ .. code-block:: console
9
+
10
+ python examples/first_run_preferences.py
11
+ python examples/reset_preferences.py
12
+
13
+ On Windows, ``.\.venv\Scripts\python.exe`` can replace ``python`` without
14
+ activating the environment. Every file in these scenarios is temporary and
15
+ removed on exit, so each invocation begins with fresh demonstration data.
16
+ To adapt a recipe to real preferences, create ``ConfigStore("your-app")``
17
+ without a temporary ``directory`` override and remove the demo's seed data.
18
+
19
+ First-run setup
20
+ ----------------
21
+
22
+ ``first_run_preferences.py`` asks for theme, language, font size, and
23
+ notifications using :class:`~upref.Field`. Parsers and validators reject invalid
24
+ choices and out-of-range sizes. Press Enter four times to accept the defaults,
25
+ or try ``purple`` as the theme and ``9`` as the font size to see retries.
26
+
27
+ File presence marks completed setup in this recipe. No file is written if
28
+ collection is cancelled. A second startup is simulated in the same process and
29
+ reuses the saved preferences without asking again. Existing values are not
30
+ business-validated on that path; see ``typed_settings.py`` when manually edited
31
+ preferences need validation on every startup.
32
+
33
+ .. literalinclude:: ../examples/first_run_preferences.py
34
+ :language: python
35
+
36
+ Editing and applying a draft
37
+ -----------------------------
38
+
39
+ ``apply_preferences.py`` starts with saved preferences and edits a detached
40
+ draft. The final yes/no prompt defaults to ``False``: Enter discards the draft,
41
+ ``yes`` saves it, and Ctrl+C cancels the interaction. A language preference
42
+ outside the editing schema survives a successful save.
43
+
44
+ For example, enter ``dark``, ``20``, and ``yes`` to apply changes. Repeat with
45
+ ``no`` to observe the unchanged saved preferences. A load/edit/save interaction
46
+ assumes one writer; applications with simultaneous writers must coordinate the
47
+ whole operation as described in :doc:`storage`.
48
+
49
+ .. literalinclude:: ../examples/apply_preferences.py
50
+ :language: python
51
+
52
+ Resetting preferences
53
+ -----------------------
54
+
55
+ ``reset_preferences.py`` removes a saved font-size override, then the entire
56
+ appearance section, then the configuration file. The intermediate output shows
57
+ that notifications remain disabled while appearance returns to defaults.
58
+
59
+ Load the raw saved mapping when removing overrides. Loading with defaults and
60
+ saving that result would persist those default values. Likewise,
61
+ ``update({"appearance": {}})`` does not clear a section because mappings merge
62
+ recursively. Remove the key from the loaded mapping and save explicitly.
63
+
64
+ .. literalinclude:: ../examples/reset_preferences.py
65
+ :language: python
66
+
67
+ Recent documents
68
+ ------------------
69
+
70
+ ``recent_files.py`` normalizes paths, moves reopened documents to the front,
71
+ and limits the history to two entries in the demonstration. The result is
72
+ ``['notes.txt', 'draft.md']``; the separate theme preference remains ``dark``.
73
+ No document is opened or created. Existing history entries are assumed to have
74
+ been normalized by this application; malformed lists are rejected before saving.
75
+
76
+ .. literalinclude:: ../examples/recent_files.py
77
+ :language: python
78
+
79
+ Window position and size
80
+ --------------------------
81
+
82
+ ``window_preferences.py`` restores a window saved on a larger monitor into a
83
+ 1280 by 720 display. Invalid value types fall back to defaults; bounds are
84
+ clamped so the restored window is visible. The example does not open a window.
85
+
86
+ The geometry model uses the primary display's usable rectangle with origin
87
+ ``(0, 0)``. For multiple monitors, obtain the actual work areas and offsets from
88
+ the GUI toolkit. Persist normal window bounds when closing, together with a
89
+ separate maximized flag; minimized bounds are not useful restoration data.
90
+
91
+ .. literalinclude:: ../examples/window_preferences.py
92
+ :language: python
93
+
94
+ Several accounts in one application
95
+ ------------------------------------
96
+
97
+ ``account_preferences.py`` stores work and personal settings under separate
98
+ account keys. Updating the work theme preserves the personal account. Unknown
99
+ accounts receive defaults, and signing out removes the active selection while
100
+ retaining the saved preferences.
101
+
102
+ Account IDs are mapping keys, not filenames or credentials. These are accounts
103
+ inside the same OS user's application, not an access-control boundary between
104
+ different OS users. The recipe uses flat account preferences; nested defaults
105
+ would need an explicit recursive merge strategy.
106
+
107
+ .. literalinclude:: ../examples/account_preferences.py
108
+ :language: python
109
+
110
+ Preferences for the current session
111
+ ------------------------------------
112
+
113
+ ``session_overrides.py`` makes precedence explicit: application defaults,
114
+ saved preferences, then command-line options. Try:
115
+
116
+ .. code-block:: console
117
+
118
+ python examples/session_overrides.py --theme dark --font-size 20
119
+ python examples/session_overrides.py --theme dark --remember
120
+
121
+ The first command prints a dark session while saved preferences remain light.
122
+ The second persists only the supplied theme override. It does not write every
123
+ application default. Both commands operate inside a temporary demo directory,
124
+ so ``--remember`` does not affect a later invocation of this example.
125
+
126
+ .. literalinclude:: ../examples/session_overrides.py
127
+ :language: python
128
+
129
+ Importing and exporting portable preferences
130
+ ---------------------------------------------
131
+
132
+ ``import_export_preferences.py`` exports only theme, language, and font size
133
+ to JSON. Machine-local document history is excluded. Import rejects unknown
134
+ keys, invalid values, duplicate keys, and malformed JSON before the application
135
+ chooses to apply the preview. Boolean font sizes are rejected explicitly.
136
+
137
+ The demonstration transfers preferences between two stores while preserving
138
+ ``['target-only.txt']`` as the target's local history. This is a small, local
139
+ file exchange example; its JSON export uses an ordinary file write. Configuration
140
+ persistence through ``ConfigStore`` continues to use atomic replacement.
141
+
142
+ .. literalinclude:: ../examples/import_export_preferences.py
143
+ :language: python
144
+
145
+ Backing up and restoring settings
146
+ ----------------------------------
147
+
148
+ ``backup_restore.py`` saves a value snapshot in a separate configuration file,
149
+ changes preferences, then restores the snapshot. A missing or malformed backup
150
+ raises before the current file is replaced. The backup itself remains unchanged.
151
+
152
+ The snapshot preserves values rather than YAML comments or formatting. This
153
+ single-writer recipe does not provide backup rotation or a transaction across
154
+ the two files. Coordinate other writers when using it in a shared application.
155
+
156
+ .. literalinclude:: ../examples/backup_restore.py
157
+ :language: python
158
+
159
+ Previewing a legacy migration
160
+ ------------------------------
161
+
162
+ ``migration_preview.py`` creates a temporary legacy file with a reminder value
163
+ and its unit. Automatic descriptor detection extracts the value but drops the
164
+ unit. ``source_format="raw"`` preserves both.
165
+
166
+ Both previews use ``dry_run=True`` and create no destination directory. The
167
+ example then explicitly applies the raw conversion and verifies that the old
168
+ file is unchanged. For migration of an actual v1 file, see ``migrate_v1.py``
169
+ and :doc:`migration`.
170
+
171
+ .. literalinclude:: ../examples/migration_preview.py
172
+ :language: python