Skip to content

Commit b03a1be

Browse files
authored
fix: multi-user machine permission errors on /tmp created dirs (#158)
* fix: multi-user machine permission errors on /tmp created dirs - each user gets /tmp/of3-of-$USERNAME - respects TMPDIR and other env vars - all the colabfold and jackhammer code is moved int - tests - fixes #150 * review: comments from Jennifer * revert jackhammer.py changes
1 parent db3e4dd commit b03a1be

7 files changed

Lines changed: 110 additions & 13 deletions

File tree

docs/source/configuration_reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ Configures the ColabFold MSA server integration.
232232
- `server_user_agent` *(str)*: User agent string (default: `openfold`)
233233
- `server_url` *(Url)*: ColabFold server URL (default: `https://api.colabfold.com`)
234234
- `save_mappings` *(bool)*: Save sequence ID mappings (default: `true`)
235-
- `msa_output_directory` *(Path)*: Directory for MSA outputs (default: temporary directory)
235+
- `msa_output_directory` *(Path)*: Directory for MSA outputs (default: `temporary directory/of3-of-<user>/colabfold_msas`)
236236
- `cleanup_msa_dir` *(bool)*: Delete MSAs after processing (default: `true`)
237237

238238
**Example**:

examples/reference_full_config/full_config.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ msa_computation_settings:
130130
server_user_agent: openfold
131131
server_url: https://api.colabfold.com/
132132
save_mappings: true
133-
msa_output_directory: <tmp-dir>/of3_colabfold_msas/
133+
msa_output_directory: <tmp-dir>/of3-of-<user>/colabfold_msas/
134134
cleanup_msa_dir: true
135135

136136
# TemplatePreprocessorSettings: https://github.com/aqlaboratory/openfold-3/blob/main/openfold3/core/data/pipelines/preprocessing/template.py#L1459
@@ -152,11 +152,11 @@ template_preprocessor_settings:
152152
create_logs: false
153153
n_processes: 1
154154
chunksize: 1
155-
structure_directory: <tmp-dir>/of3_template_data/template_structures
155+
structure_directory: <tmp-dir>/of3-of-<user>/template_data/template_structures
156156
structure_file_format: cif
157-
output_directory: <tmp-dir>/of3_template_data
157+
output_directory: <tmp-dir>/of3-of-<user>/template_data
158158
precache_directory: null
159159
structure_array_directory: null
160-
cache_directory: <tmp-dir>/of3_template_data/template_cache
160+
cache_directory: <tmp-dir>/of3-of-<user>/template_data/template_cache
161161
log_directory: null
162162
ccd_file_path: null

openfold3/core/data/pipelines/preprocessing/template.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import os
2020
import random
2121
import re
22-
import tempfile
2322
import traceback
2423
from datetime import datetime
2524
from functools import wraps
@@ -1601,9 +1600,11 @@ def _prepare_output_directories(self) -> "TemplatePreprocessorSettings":
16011600
"pipeline."
16021601
)
16031602

1604-
self.output_directory = (
1605-
self.output_directory or Path(tempfile.gettempdir()) / "of3_template_data"
1606-
)
1603+
if self.output_directory is None:
1604+
from openfold3.core.data.tools.utils import get_of3_tmpdir
1605+
1606+
self.output_directory = get_of3_tmpdir("template_data")
1607+
16071608
base = self.output_directory
16081609

16091610
# only set these if the user did not give them explicitly

openfold3/core/data/tools/colabfold_msa_server.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import random
1919
import shutil
2020
import tarfile
21-
import tempfile
2221
import time
2322
import warnings
2423
from dataclasses import dataclass, field
@@ -997,13 +996,17 @@ class MsaComputationSettings(BaseModel):
997996
server_user_agent: str = "openfold"
998997
server_url: Url = Url("https://api.colabfold.com")
999998
save_mappings: bool = True
1000-
msa_output_directory: Path = Path(tempfile.gettempdir()) / "of3_colabfold_msas"
999+
msa_output_directory: Path | None = None
10011000
cleanup_msa_dir: bool = True
10021001

10031002
@model_validator(mode="after")
10041003
def create_dir(self) -> "MsaComputationSettings":
10051004
"""Creates the output directory if it does not exist."""
1006-
if not self.msa_output_directory.exists():
1005+
if self.msa_output_directory is None:
1006+
from openfold3.core.data.tools.utils import get_of3_tmpdir
1007+
1008+
self.msa_output_directory = get_of3_tmpdir("colabfold_msas")
1009+
elif not self.msa_output_directory.exists():
10071010
self.msa_output_directory.mkdir(parents=True, exist_ok=True)
10081011
return self
10091012

openfold3/core/data/tools/utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,27 @@
1717

1818
import contextlib
1919
import datetime
20+
import getpass
2021
import logging
2122
import shutil
2223
import tempfile
2324
import time
25+
from pathlib import Path
26+
27+
28+
def get_of3_tmpdir(subdir: str | None = None) -> Path:
29+
"""Return a user-namespaced OpenFold3 temporary directory.
30+
31+
Follows the same convention as pytest (``/tmp/pytest-of-<user>/``):
32+
``<tmpdir>/of3-of-<user>/<subdir>``. Respects ``$TMPDIR`` and other
33+
platform conventions via :func:`tempfile.gettempdir`.
34+
35+
The returned directory is created on disk if it does not already exist.
36+
"""
37+
base = Path(tempfile.gettempdir()) / f"of3-of-{getpass.getuser()}"
38+
path = base / subdir if subdir else base
39+
path.mkdir(parents=True, exist_ok=True)
40+
return path
2441

2542

2643
@contextlib.contextmanager
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Tests for ``openfold3.core.data.tools.utils``."""
2+
3+
import getpass
4+
import tempfile
5+
from pathlib import Path
6+
from unittest.mock import patch
7+
8+
from openfold3.core.data.tools.utils import get_of3_tmpdir
9+
10+
11+
class TestGetOf3Tmpdir:
12+
"""Tests for get_of3_tmpdir."""
13+
14+
def test_returns_path(self):
15+
result = get_of3_tmpdir("test_subdir")
16+
assert isinstance(result, Path)
17+
18+
def test_directory_is_created(self, tmp_path):
19+
with patch.object(tempfile, "gettempdir", return_value=str(tmp_path)):
20+
result = get_of3_tmpdir("mysubdir")
21+
22+
assert result.is_dir()
23+
24+
def test_contains_username(self, tmp_path):
25+
with patch.object(tempfile, "gettempdir", return_value=str(tmp_path)):
26+
result = get_of3_tmpdir("subdir")
27+
28+
assert f"of3-of-{getpass.getuser()}" in str(result)
29+
30+
def test_subdir_appended(self, tmp_path):
31+
with patch.object(tempfile, "gettempdir", return_value=str(tmp_path)):
32+
result = get_of3_tmpdir("colabfold_msas")
33+
34+
assert result.name == "colabfold_msas"
35+
assert result.parent.name == f"of3-of-{getpass.getuser()}"
36+
37+
def test_no_subdir(self, tmp_path):
38+
with patch.object(tempfile, "gettempdir", return_value=str(tmp_path)):
39+
result = get_of3_tmpdir()
40+
41+
assert result.name == f"of3-of-{getpass.getuser()}"
42+
assert result.is_dir()
43+
44+
def test_respects_tmpdir_env(self, tmp_path, monkeypatch):
45+
custom_tmp = tmp_path / "custom_tmp"
46+
custom_tmp.mkdir()
47+
monkeypatch.setenv("TMPDIR", str(custom_tmp))
48+
# Force tempfile to re-evaluate TMPDIR
49+
tempfile.tempdir = None
50+
51+
result = get_of3_tmpdir("subdir")
52+
53+
assert str(result).startswith(str(custom_tmp))
54+
55+
def test_idempotent(self, tmp_path):
56+
with patch.object(tempfile, "gettempdir", return_value=str(tmp_path)):
57+
first = get_of3_tmpdir("subdir")
58+
second = get_of3_tmpdir("subdir")
59+
60+
assert first == second
61+
62+
def test_different_subdirs_are_isolated(self, tmp_path):
63+
with patch.object(tempfile, "gettempdir", return_value=str(tmp_path)):
64+
a = get_of3_tmpdir("aaa")
65+
b = get_of3_tmpdir("bbb")
66+
67+
assert a != b
68+
assert a.parent == b.parent
69+
70+
def test_different_users_are_isolated(self, tmp_path):
71+
with patch.object(tempfile, "gettempdir", return_value=str(tmp_path)):
72+
real = get_of3_tmpdir("data")
73+
with patch.object(getpass, "getuser", return_value="other_user"):
74+
other = get_of3_tmpdir("data")
75+
76+
assert real != other
77+
assert "of3-of-other_user" in str(other)

openfold3/tests/test_writer.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ def write_confidence_scores(
142142
def test_full_confidence_scores_written(
143143
self, tmp_path, output_fmt, dummy_confidence_scores
144144
):
145-
146145
self.write_confidence_scores(
147146
tmp_path, output_fmt, True, dummy_confidence_scores
148147
)

0 commit comments

Comments
 (0)