Skip to content

ResolvablePath

What it is

ResolvablePath is a type alias defined in lib/path_utils.py:60:

ResolvablePath = Annotated[AnyPath, _ResolveAgainstDataRoot()]

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:

  1. Resolved at construction time — relative paths are joined to data_root (the INPUT_DATA_DIR env var); s3:// URIs and absolute paths pass through unchanged. Resolution happens in _resolve_data_paths (config.py:816).
  2. Existence-checked at preflight time_iter_resolvable_fields(config) yields every (owner, field_name) pair typed ResolvablePath, and per-stage preflight functions call check_path_exists(value) on each. This is the mechanism behind DESIGN.md Clause 31: adding a new ResolvablePath field 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 value is None, skip.
  • If value is an absolute path or an s3:// 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:60ResolvablePath alias. lib/path_utils.py:62_iter_resolvable_fields implementation. config.py:816_resolve_data_paths model validator. run/preflight.py:85preflight_paths_for_resolvable_inputs caller.

Cross-references