An opened file is not a successful-write receipt
In the Python local-cache helper, _open_file
yields a handle/error pair. An opening IOError yields a missing handle; after a successful open,
ordinary Exception from the yielded body is logged and suppressed before the handle is closed.
Logging or close failures can still escape. The internal memory fallback in _FileIO._doWrite
belongs to the missing-handle branch; it is not a fallback for every write failure.
The writer discards fd.write's result and returns the handle after context cleanup.
That return does not establish that the body completed successfully or that bytes are durable.
FileLocalCache.write discards the redirect result altogether. For mod-owned persistence,
define an explicit success/failure result when callers need an acknowledgement, and keep opening,
writing, closing and durable storage as distinct boundaries.
Targeted static inspection of original Python bytecode from WG 2.4.1.1 #989 confirmed the
tuple binding; the readable dump's apparent second context manager is an extraction artifact. This evidence
covers the inspected methods, not whole-module parity or a real file-failure, atomicity or durability test.
Do not persist transient identity
Arena IDs, entity IDs, view instances, callback IDs, and dropdown indexes are temporary. Persist stable semantic values and reconstruct presentation state when the relevant system becomes ready.
Preferences-root storage
When a mod needs account-independent local storage, the client preferences path is a useful anchor because it follows the active client profile. Verify the helper in the target build, derive the containing directory, and store only files owned by your mod in a separate subdirectory.
Illustrative Python 2.7 example. The missing
_owned_storage_dir() supplies all anchoring and ownership policy. Only its
literal None selects None; otherwise the join result is returned.
An empty root can yield a relative filename. The shell creates no directory, checks no
containment or writability, performs no read/write or recovery, and propagates failures.
The helper's name does not establish a safe preferences-root storage location.
def _config_path():
root = _owned_storage_dir()
if root is None:
return None
return os.path.join(root, 'settings.json')
Schema and migration
from copy import deepcopy
DEFAULTS = {
'schema': 2,
'enabled': True,
'position': {'x': 40, 'y': 40},
}
def _load_config(path):
defaults = deepcopy(DEFAULTS)
raw = _read_json(path)
if not isinstance(raw, dict):
return defaults
raw = _migrate_config(raw)
return _validate_and_merge(raw, defaults)
Copy the JSON-shaped default tree for each load. A top-level dict(DEFAULTS)
would still share the nested position dictionary, allowing an edited result to change later
fallbacks. Pass the private copy into validation, and require custom validators to return
mutable values owned by that result rather than borrowed template subtrees.
Here the default copy is made before _read_json. Copy failure prevents reading;
reader failure propagates despite the prepared defaults. Only a returned non-dictionary selects
those defaults. An admitted raw dictionary goes to _migrate_config without copying
its tree, then the unchecked result and private defaults go to _validate_and_merge.
These helpers are undefined here. The validator's return is forwarded unchanged, without a final type, version or ownership check. Its name does not establish safe merging or validation; helpers may mutate their inputs or retain borrowed state. This shell provides no fallback on exceptions, save, replacement, retry, cleanup, traceback logging or module version. Define those owner contracts before using it as part of a mod. The separate migration-loop illustration likewise omits progress and final-version checks. These exact fragments were reviewed, not executed.
Apply migrations one schema version at a time. Write to a temporary file, flush it, then replace the destination with a defined recovery policy; staging alone does not prove crash durability.
Saved values are not recursively merged defaults
The ordinary account preference getter returns a successfully decoded saved value directly. It does not recursively merge new default keys or validate nested types. Missing/corrupt data falls back to a copied registered default; section-specific migrations can run separately. Own schema migration when adding fields and preserve the distinctions between missing, false, zero and None.
The recursive helper for new-setting counters reconciles novelty indicators, not arbitrary configuration. Follow the caller and storage owner before borrowing a merge-looking function; it is not a general migration or disk-flush API.
Write frequency
Update in-memory state immediately, but debounce disk writes from drag, slider, or rapid event streams. Flush on a bounded delay and on orderly teardown.
Security and privacy
Do not log tokens, secrets, private endpoints, or complete server payloads. Treat downloaded configuration as untrusted input with size, type, and path limits.
Common mistakes
- Replacing defaults with unvalidated JSON.
- Persisting UI indexes that change when options reorder.
- Writing on every mouse-move event.
- Using a temporary installation directory for data meant to survive updates.
- Allowing arbitrary paths from configuration or network data.
Verification trail
Confirm the client preferences directory, account identity timing, existing configuration conventions, and shutdown path. Test empty, corrupt, old-schema, read-only, and interrupted-write cases.