Skip to content

Commit 4c77457

Browse files
authored
Lock parallel package operations (#2593)
1 parent 3d50713 commit 4c77457

File tree

6 files changed

+25
-9
lines changed

6 files changed

+25
-9
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ repos:
5959
- flake8-unused-arguments==0.0.12
6060
- flake8-noqa==1.3
6161
- pep8-naming==0.13.2
62-
- flake8-pyproject==1.2.1
62+
- flake8-pyproject==1.2.2
6363
- repo: https://github.com/pre-commit/mirrors-prettier
6464
rev: "v2.7.1"
6565
hooks:

docs/changelog/2594.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Ensure that two parallel tox instance invocations on different tox environment targets will work by holding a file lock
2+
onto the packaging operations (e.g., in bash ``tox4 r -e py311 &; tox4 r -e py310``) - by :user:`gaborbernat`.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies = [
3030
"pyproject-api>=1.2.1",
3131
'tomli>=2.0.1; python_version < "3.11"',
3232
"virtualenv>=20.17",
33+
"filelock>=3.8.1",
3334
'importlib-metadata>=5.1; python_version < "3.8"',
3435
'typing-extensions>=4.4; python_version < "3.8"',
3536
]
@@ -49,7 +50,6 @@ optional-dependencies.testing = [
4950
"devpi-process>=0.3",
5051
"diff-cover>=7.2",
5152
"distlib>=0.3.6",
52-
"filelock>=3.8",
5353
"flaky>=3.7",
5454
"hatch-vcs>=0.2",
5555
"hatchling>=1.11.1",

src/tox/tox_env/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ def _clean(self, transitive: bool = False) -> None: # noqa: U100
290290
env_dir = self.env_dir
291291
if env_dir.exists():
292292
LOGGER.warning("remove tox env folder %s", env_dir)
293-
ensure_empty_dir(env_dir)
293+
ensure_empty_dir(env_dir, except_filename="file.lock")
294294
self._log_id = 0 # we deleted logs, so start over counter
295295
self.cache.reset()
296296
self._run_state.update({"setup": False, "clean": True})

src/tox/tox_env/package.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from types import MethodType
1010
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, cast
1111

12+
from filelock import FileLock
13+
1214
from tox.config.main import Config
1315
from tox.config.sets import EnvConfigSet
1416

@@ -31,29 +33,39 @@ def __str__(self) -> str:
3133
return str(self.path)
3234

3335

34-
def _lock_method(lock: RLock, meth: Callable[..., Any]) -> Callable[..., Any]:
36+
def _lock_method(thread_lock: RLock, file_lock: FileLock | None, meth: Callable[..., Any]) -> Callable[..., Any]:
3537
def _func(*args: Any, **kwargs: Any) -> Any:
36-
with lock:
37-
return meth(*args, **kwargs)
38+
with thread_lock:
39+
if file_lock is not None and file_lock.is_locked is False: # file_lock is to lock from other tox processes
40+
file_lock.acquire()
41+
try:
42+
return meth(*args, **kwargs)
43+
finally:
44+
if file_lock is not None:
45+
file_lock.release()
3846

3947
return _func
4048

4149

4250
class PackageToxEnv(ToxEnv, ABC):
4351
def __init__(self, create_args: ToxEnvCreateArgs) -> None:
44-
self._lock = RLock()
52+
self._thread_lock = RLock()
53+
self._file_lock: FileLock | None = None
4554
super().__init__(create_args)
4655
self._envs: set[str] = set()
4756

4857
def __getattribute__(self, name: str) -> Any:
4958
# the packaging class might be used by multiple environments in parallel, hold a lock for operations on it
5059
obj = object.__getattribute__(self, name)
5160
if isinstance(obj, MethodType):
52-
obj = _lock_method(self._lock, obj)
61+
obj = _lock_method(self._thread_lock, self._file_lock, obj)
5362
return obj
5463

5564
def register_config(self) -> None:
5665
super().register_config()
66+
file_lock_path: Path = self.conf["env_dir"] / "file.lock"
67+
self._file_lock = FileLock(file_lock_path)
68+
file_lock_path.parent.mkdir(parents=True, exist_ok=True)
5769
self.core.add_config(
5870
keys=["package_root", "setupdir"],
5971
of_type=Path,

src/tox/util/path.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from shutil import rmtree
55

66

7-
def ensure_empty_dir(path: Path) -> None:
7+
def ensure_empty_dir(path: Path, except_filename: str | None = None) -> None:
88
if path.exists():
99
if path.is_dir():
1010
for sub_path in path.iterdir():
11+
if sub_path.name == except_filename:
12+
continue
1113
if sub_path.is_dir():
1214
rmtree(sub_path, ignore_errors=True)
1315
else:

0 commit comments

Comments
 (0)