Derive from pre-made time series, and check directory permissions (#431) - #481
Conversation
Two gaps that get in the way of running the ADF on files someone else produced (NCAR#431), neither of which needs a change to how the ADF thinks about file locations. Derived variables were welded to the time series *creation* step: `check_derive` decides derivability by looking for constituents in a history file, and `create_time_series` skips that whole step when `cam_ts_done` is true. A pre-made set of time series therefore never gained its derived variables -- with FSNT and FLNT sitting in the directory and RESTOM requested, the climatology step reported RESTOM as having no time series files and the run still ended "successfully". That happens in the user's own directory too, so it was never really about whose files they are. `derive_from_premade_ts` derives from the time series that are present instead, so the constituents alone are enough, and both time series back ends now call it. Nothing in lib/ or scripts/ called `os.access`, so a directory this user cannot read was reported as a directory with no history files in it -- `is_dir()` returns True for one and its glob comes back empty, which sends the user looking for missing data that is really just unreadable. `describe_dir_problem` distinguishes the cases and each caller keeps its own config-key hint. It also guards the two places the ADF writes: a `cam_ts_loc` that cannot be written now fails immediately with the reason, rather than letting every ncrcat fail unexamined, and a derived variable that would have to be written into someone else's directory is reported as unavailable instead of raising PermissionError partway through a run. Fixes part of NCAR#431; the read-locations vs. write-location split discussed there is not needed for either of these. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deriving from pre-made time series did nothing, silently, when no history stream was configured. That is a legal and ordinary setting for 'cam_ts_done: true' -- there are no history files to name a stream for, and adf_info leaves 'base_hist_str' empty in exactly that case, saying in a comment that the searches downstream are then meant to match any stream. Iterating an empty string yields nothing, so the loop body never ran. find_constit and derive_variable both take 'hist_str=None' to mean "any stream", so fall back to that. A variable can declare both 'derivable_from_cam_chem' and 'derivable_from'; SO4 and SOA do. check_derive takes the CAM-CHEM list only when every one of its constituents is present, and falls back to the plain CAM list otherwise. Preferring the CAM-CHEM list outright asked an ordinary CAM run for constituents it never wrote, so SO4 and SOA could not be derived at all. _premade_constits makes the same choice from the time series files. The 'cam_ts_loc' write check ran before any per-variable work, so it failed a run that writes nothing because every file is already there and 'cam_overwrite_ts' is false -- a read-only directory of finished time series, which works today. Check where a write is actually about to happen instead, in both back ends. Also: report which variables are missing only when something really has to be written, so a read-only directory that already holds the derived variable is not complained about; distinguish an unreachable directory from a missing one when the parent is not searchable; and record the new dependencies in the GenTS docstring. test_premade_ts_derive.py covers the derivation path against a stub, so no config file or history files are needed. Four of its checks fail if the first two defects above are reintroduced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round: three defects found and fixed (
|
The new tests imported numpy, xarray and adf_diag at module level, so collecting them failed the unit test workflow on all five python versions: it installs only PyYAML and pytest. Guard the imports and skip the class the way test_adf_derive.py already does, and guard the class body too, since it borrows the methods under test off AdfDiag and runs even when the tests themselves are skipped. Checked against a venv holding only PyYAML and pytest, the way CI builds it: 69 passed, 68 skipped, no collection error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses the two tractable parts of #431. Neither needs a change to how the ADF thinks about file locations.
1. Pre-made time series never gained their derived variables
Derivation was welded to the time series creation step:
check_derivedecides derivability by looking for constituents in a history file (lib/adf_derive.py), andcreate_time_seriesskips that whole step whencam_ts_doneis true. So a pre-made set of time series could never gain a derived variable — and this happens in your own directory too, which is why it turned out not to be about whose files they are.Before, with
FSNTandFLNTsitting in the directory andRESTOMindiag_var_list:A silently missing variable and a zero exit code.
AdfDiag.derive_from_premade_tsnow derives from the time series that are present, so the constituents alone are enough. Both back ends call it — the built-in one and GenTS.2. Nothing checked permissions
Nothing in
lib/orscripts/calledos.access. A directory this user cannot read reportsis_dir() == Trueand then globs to nothing, so the ADF told the user their history files were missing when they were merely unreadable — sending them to look for the wrong problem:cam_hist_locNo history files found for [['cam.h0']]!'...' exists but this user does not have permission to read it!describe_dir_problemlives inlib/adf_file_utils.py— the stdlib-only module, so it is unit tested in CI — and each caller keeps its own config-key hint. It is used at five read sites (adf_diag.py,adf_gents.py, and three inadf_info.py) and guards the three places the ADF writes:cam_ts_locthat cannot be written now fails immediately with the reason and the remedy, instead of letting everyncrcatfail unexamined (subprocess.runthere has nocheck=, so those failures are invisible) and reporting the files as missing much later;PermissionErrorpartway through a run:That last message is the honest statement of what the ADF can and cannot do here, and it points at the configuration that already works today: read someone else's history files, write your own time series. Derived variables have never been a problem in that arrangement, because everything the ADF writes goes to your own space.
No new config keys
derivable_from/derivable_from_cam_chemare read from the existing variable defaults, so no config file changes and every existing config keeps working._find_constitbecamefind_constitnow that it has a caller outsideadf_derive.Testing
Unit tests (5 new, in
test_adf_file_utils.py, stdlib-only so CI runs them): a usable directory, a missing path, a file where a directory was wanted, an unreadable directory — asserting the misleadingis_dir() == True/ empty-glob behavior that motivates the helper — and a read-only directory that is fine to read but reported whenneed_write=True. Both permission tests skip as root.End to end on Casper, model-vs-model, two dissimilar cases (a CESM3 alpha 1850 control on
cam.h0a, 93 levels; an AMIP run oncam.h0, 32 levels),cam_ts_done: trueagainst pre-made time series:RESTOMderived for both cases and bothRESTOMclimo files written, where before both were silently absentmax |RESTOM - (FSNT - FLNT)| == 0.0,unitsandlong_nameset, constituents dropped from the derived filerc=0cam_hist_loc: the accurate message, immediatelycam_ts_locwithcam_ts_done: false: fails immediately with the reasonpytest lib/test/unit_tests151 passed.pylint --rcfile=lib/test/pylintrc9.80/10 on bothlib/adf_diag.pyandlib/adf_info.py(the two CI-linted files touched; gate is 9.5) with no new messages inside the changed hunks.darker --check --revision upstream/main lib/clean.Still open on #431
The read-locations vs. write-location split — the catalog idea — is untouched, and neither of these needed it. The remaining limitation is the one the warning names: a derived variable cannot be added to a directory you cannot write in.
lib/adf_file_utils.py'sfind_ts_filesis the single chokepoint for time series lookup, so if that turns out to matter in practice, an optional writable location defaulting to the current directory is a small follow-up rather than a rewrite.🤖 Generated with Claude Code