Plan: timeout Propagation to check_output + os.environ Cache-Key Narrowing
Implements
docs/superpowers/specs/2026-07-21-check-output-timeout-design.md.
Two intertwined changes, done together because they touch the same
functions and the same @cache-key surface:
- Thread
timeout: float | None = Nonethrough every function whose call chain reachescheck_output/check_call. - Replace the four
**os.environcache-key-widening splats with**get_prefixed_environ("DATABRICKS_" | "OP_").
Sequencing
Bottom-up per module, tests alongside each function (not deferred to the
end) — confirming TimeoutExpired propagates one layer at a time is far
easier than debugging the whole chain after every function has changed.
make format && make test after every step.
subprocess.pycleanup._utilities.py:timeouton the three leaf functions, plus the newget_prefixed_environhelper.databricks.py, bottom-up.onepassword.py, bottom-up.- Full-suite verification.
Step 1 — subprocess.py cleanup
- Normalize parameter order across all three
@overloadstubs and the implementation ofcheck_outputtocwd, input, env, suppress_stderr, shell, timeout, echo(timeoutcurrently sits in an inconsistent position in one overload). check_output/check_callthemselves already havetimeoutwired tosubprocess.run— no functional change here, ordering only.- Test: confirm
tests/test_subprocess.pycoverstimeoutraisingsubprocess.TimeoutExpiredon bothcheck_outputandcheck_call(via a real long-running command), and that a generous timeout doesn't change a successful result vs. an untimed call. Add if missing.
Step 2 — _utilities.py
In call order (each depends on the previous within this file):
install_brew(timeout: float | None = None)→ forward to its onecheck_outputcall.which_brew(timeout=None)→ forward to bothcheck_outputcalls and toinstall_brew(timeout=timeout).which_winget(timeout=None)→ forward to its onecheck_outputcall.- New:
get_prefixed_environ(*prefixes: str) -> dict[str, str](see spec for body) — no dependents yet, but lives here sodatabricks.py/onepassword.pycan import it in steps 3–4.
Tests (tests/test__utilities.py):
- timeout forwarding for install_brew/which_brew/which_winget:
near-zero timeout against the real brew/winget/install-script
invocation raises TimeoutExpired, platform-gated like the existing
tests for these.
- get_prefixed_environ: pure unit test (no external resource, no
mocking restriction) — single prefix, multiple prefixes, no match →
empty dict.
Step 3 — databricks.py, bottom-up
Add timeout: float | None = None and forward at each level, in this
order (matches the call graph):
_install_sh_databricks_cli— forward to itscheck_call._install_databricks_cli— forward towhich_winget,which_brew, its fourcheck_outputcalls, and_install_sh_databricks_cli.which_databricks— forward to itscheck_outputcall and to_install_databricks_cli._databricks_auth_profiles(@cache) — forward towhich_databricksand itscheck_outputcall._get_host_profile(@cache) — forward to_databricks_auth_profiles._databricks_auth_describe— finish the partial work already in the working tree: forwardtimeouton theelse:branch'scheck_outputcall too (currently missing), and on the now-timeout-awarewhich_databricks()/_get_host_profile(host)calls._databricks_bundle_summary— forward towhich_databricksand itscheck_outputcall._databricks_auth_login_target— forward to_databricks_auth_loginand_databricks_bundle_summary._databricks_auth_login(@cache+@retry) — forward towhich_databricks,_get_host_profile, bothcheck_callcalls,_databricks_auth_login_target,_databricks_auth_profiles, and the recursive self-call.databricks_auth_login— finish the partial work already present: fix the duplicatedRaises: CalledProcessErrordocstring line and trailing whitespace; letmake formatsettle the wrapping of the final_databricks_auth_login(...)call. Also: replace**os.environwith**get_prefixed_environ("DATABRICKS_")at this call site._get_env_databricks_workspace_client(@cache) — addtimeout, forward todatabricks_auth_login(this call currently only passeshost/profile).get_databricks_workspace_client— addtimeout, forward. Also: replace**os.environwith**get_prefixed_environ("DATABRICKS_")at its_get_env_databricks_workspace_client(...)call.get_dbutils— addtimeout, forward._get_secret(@cache) — addtimeout, forward.get_databricks_secret— addtimeout, forward. Also: replace**os.environwith**get_prefixed_environ("DATABRICKS_")at its_get_secret(...)call._get_scope_key_secret— addtimeout, forward toget_databricks_secret.DatabricksWorkspaceClientArguments— addtimeout: float | None = Nonefield.apply_databricks_secrets_argumentsneeds no other change: it already unpacks the dataclass viaasdict(...)into thepartial(_get_scope_key_secret, ...).main()(CLI) — add--timeout FLOATto bothinstallandgetargparse subcommands; forward to_install_databricks_cli(timeout=...)/get_databricks_secret(..., timeout=...).
Tests (tests/test_databricks.py), added alongside the corresponding
step above rather than all at once:
- timeout forwarding: near-zero timeout against the real CLI
(databricks_env fixture) raises TimeoutExpired for
which_databricks, _databricks_auth_profiles, _databricks_auth_describe,
databricks_auth_login, get_databricks_secret. Generous timeout still
returns the same successful result as the existing untimed tests.
- apply_databricks_secrets_arguments: a DatabricksWorkspaceClientArguments(timeout=...)
with a near-zero value causes the decorated function's lookup to
raise/propagate TimeoutExpired (or surface via ArgumentsResolutionError,
matching how other callback failures already surface).
- Cache-key regression guard (pick one @cached function, e.g.
_databricks_auth_login): two calls with different timeout values
each do a real round-trip; two calls with the same timeout hit cache.
- Cache-key narrowing regression guard: for get_databricks_secret,
changing an irrelevant env var (e.g. PATH) between two otherwise
identical calls still hits cache; changing a DATABRICKS_* var busts
it.
Step 4 — onepassword.py, bottom-up
_install_op— forwardtimeouttowhich_winget,which_brew, bothcheck_outputcalls.which_op— forward to itscheck_outputcall and to_install_op._op_signin(@cache) — forward towhich_opand itscheck_outputcall.iter_op_account_list— forward towhich_opand itscheck_outputcall.op_signin— forward to_op_signin,iter_op_account_list,which_op.async_read_onepassword_secret(@alru_cache) — forward toop_signin,which_op, itscheck_outputcall. No change needed to the_async_resolve_resource/_async_resolve_connect_resourcebranches (nocheck_outputthere)._read_onepassword_secret(@cache) — same as above.get_onepassword_secret/read_onepassword_secretalias — addtimeout, forward to_read_onepassword_secret. Also: replace**os.environwith**get_prefixed_environ("OP_")at this call site.ApplyOnepasswordArgumentsOptions— addtimeout: float | None = Nonefield; thread it through the existing conditional-kwargspartial(read_onepassword_secret_, ...)/partial(async_read_onepassword_secret_, ...)construction inapply_onepassword_arguments(same pattern asaccount/token/host).main()(CLI) — add--timeout FLOATto bothinstallandgetsubcommands; forward to_install_op(timeout=...)/read_onepassword_secret(..., timeout=...).
Tests (tests/test_onepassword.py), alongside each step:
- timeout forwarding for which_op, op_signin, iter_op_account_list,
get_onepassword_secret/read_onepassword_secret,
async_read_onepassword_secret (onepassword_vault fixture),
near-zero → TimeoutExpired, generous → unchanged successful result.
- apply_onepassword_arguments with ApplyOnepasswordArgumentsOptions(timeout=...),
same pattern as the Databricks decorator test above.
- Cache-key regression guard for _op_signin (or _read_onepassword_secret):
different timeout → real round-trip each time; same timeout → cache
hit.
- Cache-key narrowing regression guard for get_onepassword_secret:
irrelevant env var change still hits cache; an OP_* var change busts
it.
Out of scope (per spec)
callback.py— unchanged._async_resolve_resource,_async_resolve_connect_resource,_resolve_connect_resource— HTTP clients, notcheck_output.environment.py,defaults.py,errors.py,utilities.py— nocheck_outputin their call chains.
Verification
make formatandmake testafter each of the four steps, not just at the end.- Confirm no
# pragma: no covermarker is silently masking a new, untested branch introduced by thetimeout/env-narrowing changes. - Spot-check that
_databricks_auth_login's and_read_onepassword_secret's cache behavior (bothtimeout-key-widening and the env-prefix narrowing) matches the "Cache-key regression guard" tests above — these two are the highest-risk spots for a silent cache-correctness regression.