Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion exca/steps/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,8 @@ def _prepare(self, step: Step, batch: items.StepItems) -> ComputeBatch:
upstream = tuple(batch._upstream) + tuple(step._uid_steps())
paths = step._make_paths(upstream)
if paths.step_folder not in self._checked_configs:
identity.write_configs(paths.step_folder, upstream)
# match the 0o777 applied to the cache data files (see _cache_dict)
identity.write_configs(paths.step_folder, upstream, permissions=0o777)
self._checked_configs.add(paths.step_folder)
cd = self._cache_dict(paths.cache_folder, cache_type=paths.cache_type)
mode = _fold_modes(batch._mode, _effective_mode(step))
Expand Down
8 changes: 7 additions & 1 deletion exca/steps/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,17 @@ def write_configs(
aligned_steps: tp.Sequence[Step],
*,
write: bool = True,
permissions: int | None = None,
) -> None:
"""Idempotent: writes/checks `uid.yaml`, `full-uid.yaml`, `config.yaml`.

The config is the full computation path (aligned chain), so a chain
and its last step write identical configs when sharing a folder.

When *permissions* is set, the written config yamls are chmod-ed to it
(best-effort), matching the permissions applied to the cache data files.
"""
step_folder.mkdir(exist_ok=True, parents=True)
utils.ConfigDump(model=list(aligned_steps)).check_and_write(step_folder, write=write)
utils.ConfigDump(model=list(aligned_steps)).check_and_write(
step_folder, write=write, permissions=permissions
)
31 changes: 31 additions & 0 deletions exca/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,3 +672,34 @@ def boom(**_kwargs: tp.Any) -> tp.Any:
ex = utils.make_pool_executor("processpool", max_workers=2)
assert isinstance(ex, concurrent.futures.ThreadPoolExecutor)
ex.shutdown()


class _PermModel(BaseModel):
param: int = 12


def test_config_dump_permissions(tmp_path: Path) -> None:
import stat

folder = tmp_path / "step"
folder.mkdir()
# write=True writes uid/full-uid/config yamls; permissions chmods them
utils.ConfigDump(model=_PermModel(param=7)).check_and_write(
folder, write=True, permissions=0o777
)
written = list(folder.glob("*.yaml"))
assert written # at least uid.yaml is written
for fp in written:
assert stat.S_IMODE(fp.stat().st_mode) == 0o777


def test_config_dump_no_permissions_leaves_mode(tmp_path: Path) -> None:
import stat

folder = tmp_path / "step"
folder.mkdir()
utils.ConfigDump(model=_PermModel(param=7)).check_and_write(folder, write=True)
written = list(folder.glob("*.yaml"))
assert written
# without a permissions arg, files are not force-chmod-ed to 0o777
assert all(stat.S_IMODE(fp.stat().st_mode) != 0o777 for fp in written)
18 changes: 17 additions & 1 deletion exca/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,11 +709,17 @@ def _to_yaml(self, name: str) -> str:
def _error(self, msg: str) -> RuntimeError:
return RuntimeError(f"{msg}\n\n(this is for object: {self.model!r})")

def check_and_write(self, folder: Path, *, write: bool = True) -> None:
def check_and_write(
self, folder: Path, *, write: bool = True, permissions: int | None = None
) -> None:
"""Check config consistency and optionally write files.

Raises RuntimeError if uid.yaml doesn't match (cache collision)
or defaults changed incompatibly.

When *permissions* is set, the config yamls are chmod-ed to it
(best-effort) after writing, since ``Path.write_text`` alone is capped
by the umask (``0o666``).
"""
from .confdict import ConfDict # avoid circular import

Expand Down Expand Up @@ -783,3 +789,13 @@ def read_file(name: str) -> str | None:
continue
with temporary_save_path(fp) as tmp:
Path(tmp).write_text(self._to_yaml(name), encoding="utf8")
if permissions is not None:
for name in ("uid", "full-uid", "config"):
fp = folder / f"{name}.yaml"
if not fp.exists():
continue
try:
fp.chmod(permissions)
except OSError as e: # best-effort: not fatal for a shared dir
msg = "Failed to set permissions %o on '%s': %s"
logger.warning(msg, permissions, fp, e)
Loading