Validate and merge
from copy import deepcopy
DEFAULTS = {
'schema_version': 2,
'enabled': True,
'position': {'x': 0.5, 'y': 0.5},
'display_mode': 'compact',
}
def load_settings(path):
raw = read_json(path)
if not isinstance(raw, dict):
return deepcopy(DEFAULTS)
raw = migrate(raw)
result = deepcopy(DEFAULTS)
if isinstance(raw.get('enabled'), bool):
result['enabled'] = raw['enabled']
if raw.get('display_mode') in ('compact', 'detailed'):
result['display_mode'] = raw['display_mode']
position = raw.get('position')
if isinstance(position, dict):
result['position'] = validate_position(position)
return result
These defaults contain nested JSON data, so each fallback and merge result receives a deep copy. Mutating one result's position must not change the schema template or the next load. A custom validator that supplies a replacement subtree must also define who owns that value.
read_json, migrate and validate_position are supplied
helpers. Reader failures propagate; only a returned non-dictionary selects copied defaults.
For a dictionary, migration runs before the default copy and its result is not type-checked again.
Missing or wrongly typed recognized fields retain defaults; unknown keys are not copied. The
returned schema_version stays at the template's 2, which does not prove
migration reached that version. Boolean admission uses isinstance(..., bool);
display mode is selected by membership in the two choices.
A dictionary position is passed directly to its validator, whose return replaces the default subtree without another type, range, finite-number or ownership check. It may mutate that input or return borrowed state. Define that contract explicitly. The loader supplies no file write, backup recovery, exception handler, diagnostics or module version; its integrating owner must provide traceback-bearing reporting and attribution.
The current AccountSettings API likewise distinguishes
access paths: its normal getter copies the registered default when a stored value is missing, while
getSettingsDefault returns the registry object itself (or None).
Treat mutable values from that direct accessor as borrowed, rather than assuming every
default lookup creates a defensive snapshot. Selected example/API controls used nested
dictionaries and a fake preference section, without reading native preferences.
Schema migrations
- Store an explicit schema version when key names, nesting, or value meaning can change.
- Apply one migration step at a time and require the expected source version.
- Validate the final shape before replacing the active file.
- Keep a one-time legacy migration separate from normal reads and writes.
def migrate(data):
version = data.get('schema_version', 0)
while version < CURRENT_SCHEMA:
migrator = MIGRATIONS.get(version)
if migrator is None:
raise ValueError('Unsupported settings schema: %r' % version)
data = migrator(dict(data))
version = data.get('schema_version')
return data
The loop requires surrounding CURRENT_SCHEMA and MIGRATIONS
definitions. It uses 0 only when the initial version key is missing, with no
type/domain check before comparison or lookup. A false comparison returns the supplied root
unchanged, including a comparable future version. No exact-final-version check rejects it.
Only a missing migrator represented by None triggers the explicit
ValueError; another noncallable value fails at the call. Each migrator receives a
shallow dict(data): nested mutable state is still borrowed and may change even if
a later step fails. Its returned object must support .get, with no explicit root
check. The next version has no default.
The code does not require an increase, a one-step transition or a supported final version, and imposes no iteration/time bound. A helper can skip steps, overshoot or keep the loop running. Helper failures and side effects have no rollback or diagnostic handler here. The one-step, source-version and final-shape rules above are requirements for a completed owner, not checks implemented by this illustration. This is authored control-flow review; earlier selected default/getter controls did not test this entire migration pipeline.
Typed game-parameter models
Native game-parameter schemas sit beside mod-owned persistence. Keep their three layers separate: the Account raw server-settings dictionary, the lobby context's copied settings wrapper, and the process-wide schema manager's typed models.
- Registration stores metadata but does not materialize a model. Register once before the first account or replay settings load; late registration cannot bootstrap from an already-held raw dictionary.
- Client deserialization applies field scope and conversion, but current defaults skip several strict validations. “Typed” does not mean fully validated.
- Live updates replace the complete model and emit the config-model event. Re-resolve the model instead of retaining an old instance.
- Missing or
Nonevalues do not delete an existing model in the current source. Disconnect clearing does; consumers must tolerate absence and reconnection. - Replay participation is registered separately from current model readiness.
Typed config registration, replacement and absence
The raw account server-settings dictionary, lobby ServerSettings wrapper and schema-manager model are separate representations. Registering a GameParamsSchema stores metadata; it does not materialize an already-held raw value. Registration must precede the qualifying settings set. A later update cannot necessarily bootstrap a model that was never set.
Typed conversion is not strict validation by default: client conversion skips some checks while converters, required fields and constructors still run. Field-scope filtering can yield an access-denied sentinel rather than an ordinary default. Do not treat every model field as safely readable or every typed payload as fully validated.
Live updates replace model instances. Re-resolve after the model-update event and disconnect/reconnection. Missing keys or None are not a generic deletion operation. Wrapper replacement can publish model changes before the new lobby wrapper is installed, so choose the event belonging to the representation being read.
Replay-relevant schema keys are collected at registration independently of model readiness. Clearing session models does not remove that registration. Register in time for replay capture, and keep native schema lifecycle separate from mod-owned schema migration.
Section selection happens before field defaults
Configuration precedence can select a whole section rather than merge individual fields. The persistent-cache settings loader takes the first present section from scripts configuration, engine configuration, then development preferences when development resources permit that source. It parses only the selected section and calls its schema once. An empty or partial higher-priority section still masks lower-priority sections; absent fields use schema defaults instead. Here those defaults are enabled=True, devEnabled=False and useThread=True.
For example, a selected section containing only useThread=False does not inherit enabled=False from a lower-priority source. If parsing or deserialization raises, this function does not retry another source. For owned configuration, define whether precedence selects sections or fields, and whether invalid selected data should fail or fall back. The startup consumer also applies separate version, development-mode, argument and fault-tolerance gates; the settings object alone does not prove cache initialization.
Selected-function controls with inert section mappings, parser and schema adapters checked selection, masking and exception propagation. The declared schema defaults and startup gates were inspected separately; these controls did not execute the real schema, parse XML, read configuration files, start a cache worker or call native/client APIs.