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.
- {upref-2.1.0 → upref-2.2.0}/CHANGELOG.md +42 -0
- {upref-2.1.0 → upref-2.2.0}/MANIFEST.in +1 -0
- {upref-2.1.0 → upref-2.2.0}/PKG-INFO +21 -2
- {upref-2.1.0 → upref-2.2.0}/README.md +18 -1
- {upref-2.1.0 → upref-2.2.0}/docs/concepts.rst +6 -2
- {upref-2.1.0 → upref-2.2.0}/docs/development.rst +32 -5
- {upref-2.1.0 → upref-2.2.0}/docs/examples.rst +39 -5
- {upref-2.1.0 → upref-2.2.0}/docs/index.rst +1 -0
- {upref-2.1.0 → upref-2.2.0}/docs/migration.rst +46 -3
- {upref-2.1.0 → upref-2.2.0}/docs/recipes.rst +4 -0
- {upref-2.1.0 → upref-2.2.0}/docs/releasing.rst +3 -3
- {upref-2.1.0 → upref-2.2.0}/docs/review.rst +25 -0
- {upref-2.1.0 → upref-2.2.0}/docs/security.rst +16 -0
- {upref-2.1.0 → upref-2.2.0}/docs/storage.rst +4 -0
- upref-2.2.0/docs/user_preferences.rst +172 -0
- {upref-2.1.0 → upref-2.2.0}/examples/README.md +42 -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/first_run_preferences.py +69 -0
- upref-2.2.0/examples/import_export_preferences.py +89 -0
- {upref-2.1.0 → upref-2.2.0}/examples/migrate_v1.py +4 -0
- upref-2.2.0/examples/migration_preview.py +36 -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/session_overrides.py +59 -0
- upref-2.2.0/examples/window_preferences.py +44 -0
- {upref-2.1.0 → upref-2.2.0}/pyproject.toml +4 -1
- {upref-2.1.0 → upref-2.2.0}/pytest.ini +4 -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.1.0 → upref-2.2.0}/tests/test_examples.py +30 -1
- {upref-2.1.0 → upref-2.2.0}/tests/test_legacy.py +102 -0
- {upref-2.1.0 → upref-2.2.0}/tests/test_paths.py +20 -0
- upref-2.2.0/tests/test_preference_examples.py +210 -0
- {upref-2.1.0 → upref-2.2.0}/tests/test_prompt.py +23 -1
- upref-2.2.0/tests/test_properties.py +92 -0
- {upref-2.1.0 → upref-2.2.0}/tests/test_public_api.py +1 -1
- {upref-2.1.0 → upref-2.2.0}/tests/test_storage.py +75 -6
- {upref-2.1.0 → upref-2.2.0}/tests/test_types.py +46 -0
- {upref-2.1.0 → upref-2.2.0}/upref/__init__.py +1 -1
- {upref-2.1.0 → upref-2.2.0}/upref/_paths.py +4 -1
- {upref-2.1.0 → upref-2.2.0}/upref/_storage.py +48 -28
- {upref-2.1.0 → upref-2.2.0}/upref/_types.py +14 -6
- {upref-2.1.0 → upref-2.2.0}/upref/core.py +20 -6
- {upref-2.1.0 → upref-2.2.0}/upref/legacy.py +26 -8
- {upref-2.1.0 → upref-2.2.0}/upref.egg-info/PKG-INFO +21 -2
- {upref-2.1.0 → upref-2.2.0}/upref.egg-info/SOURCES.txt +16 -0
- {upref-2.1.0 → upref-2.2.0}/upref.egg-info/requires.txt +2 -0
- {upref-2.1.0 → upref-2.2.0}/.gitattributes +0 -0
- {upref-2.1.0 → upref-2.2.0}/LICENSE.md +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/api.rst +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/changelog.md +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/conf.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/errors.rst +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/layout/extra.css +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/layout/tower.png +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/legacy_api.rst +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/license_link.md +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/paths.rst +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/prompting.rst +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/readme_link.md +0 -0
- {upref-2.1.0 → upref-2.2.0}/docs/troubleshooting.rst +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/basic_store.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/boolean_collection.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/custom_interface.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/defaults_and_update.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/edit_settings.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/environment_profiles.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/gui_collection.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/handle_errors.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/multiple_projects.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/nested_collection.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/portable_store.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/project_variables.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/schema_upgrade.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/tty_collection.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/examples/typed_settings.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/make.bat +0 -0
- {upref-2.1.0 → upref-2.2.0}/scripts/add_license_headers.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/scripts/bootstrap.ps1 +0 -0
- {upref-2.1.0 → upref-2.2.0}/setup.cfg +0 -0
- {upref-2.1.0 → upref-2.2.0}/setup.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/tests/test_core.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/tests/test_documentation.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/tests/test_license_headers.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/tests/test_merge.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/tests/test_tty.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/upref/_merge.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/upref/errors.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/upref/gui.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/upref/prompt.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/upref/py.typed +0 -0
- {upref-2.1.0 → upref-2.2.0}/upref/resources/__init__.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/upref/resources/tower.ico +0 -0
- {upref-2.1.0 → upref-2.2.0}/upref/tty.py +0 -0
- {upref-2.1.0 → upref-2.2.0}/upref.egg-info/dependency_links.txt +0 -0
- {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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: upref
|
|
3
|
-
Version: 2.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
7
|
-
the project installed in ``.venv``.
|
|
8
|
-
or automatically cleaned temporary directories.
|
|
9
|
-
below use distinct per-user configuration
|
|
10
|
-
existing historical file and creates a separate
|
|
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
|
|
|
@@ -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.
|
|
71
|
-
|
|
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.
|
|
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.
|
|
71
|
-
git push origin v2.
|
|
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
|