ResolvablePath¶
What it is¶
ResolvablePath is a type alias defined in lib/path_utils.py:60:
It is a cloudpathlib.AnyPath annotated with a sentinel class (_ResolveAgainstDataRoot) that Pydantic recognises during the ExperimentConfig model_validator(mode="after"). Every config field typed as ResolvablePath is automatically:
- Resolved at construction time — relative paths are joined to
data_root(theINPUT_DATA_DIRenv var);s3://URIs and absolute paths pass through unchanged. Resolution happens in_resolve_data_paths(config.py:816). - Existence-checked at preflight time —
_iter_resolvable_fields(config)yields every(owner, field_name)pair typedResolvablePath, and per-stage preflight functions callcheck_path_exists(value)on each. This is the mechanism behind DESIGN.md Clause 31: adding a newResolvablePathfield automatically extends preflight coverage without touching any preflight function.
How resolution works¶
_iter_resolvable_fields (lib/path_utils.py:62) walks the full Pydantic model tree — including nested sub-models, dict values, and list values — and yields pairs for every ResolvablePath-annotated field. The resolver (_resolve_data_paths validator) then calls resolve_data_path(value, data_root) on each:
- If
valueisNone, skip. - If
valueis an absolute path or ans3://URI, leave it unchanged. - Otherwise, return
AnyPath(data_root) / value.
Why it matters¶
Before ResolvablePath existed, preflight check lists were hand-maintained parallel inventories of paths to check. A new config field would silently escape preflight unless the developer remembered to add it. With ResolvablePath, the annotation itself IS the registration — no separate maintenance is needed.
This is also why AliasChoices("INPUT_DATA_DIR", "data_root") on the data_root field exists: config_resolved.yaml records the fully-resolved absolute path under the data_root key so that ExperimentResult.from_run_dir can reload the config from a different machine without needing the env var set to the same value.
Where it is used¶
| Config class | ResolvablePath fields |
|---|---|
BaseBuilderConfig |
filepath |
StressBuilder |
indices_zarr |
StressBuilder |
geo_lookup_path (optional) |
ForecastConfig |
raw_obs_filepath, materialised_climo_filepath |
All builder filepath fields and the two forecast observation paths are ResolvablePath. The mlflow_tracking_uri (sqlite path) is NOT — it has special handling to avoid resolving the sqlite file under an S3 root.
S3 path safety¶
DESIGN.md Clause 27 mandates AnyPath for all file/directory paths that may originate from S3. ResolvablePath inherits this — its underlying type is AnyPath, so a field typed ResolvablePath transparently accepts both s3:// URIs and local Paths. Wrapping an S3Path in pathlib.Path(...) is forbidden (see s3_path_safety concept).
Source citations¶
lib/path_utils.py:60 — ResolvablePath alias.
lib/path_utils.py:62 — _iter_resolvable_fields implementation.
config.py:816 — _resolve_data_paths model validator.
run/preflight.py:85 — preflight_paths_for_resolvable_inputs caller.
Cross-references¶
- Pipeline: preflight — how
ResolvablePathdrives per-stage check sets - Concept: s3_path_safety — complementary constraint on
AnyPathusage - Concept: input_data_dir_contract — the
data_root/INPUT_DATA_DIRanchor - Source: orchestration —
config.pyandpath_utils.pycode detail