Skip to content

Commit

Permalink
Lock parallel package operations
Browse files Browse the repository at this point in the history
This ensures that two tox invocation on different target environments will work.

Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
  • Loading branch information
gaborbernat committed Dec 5, 2022
1 parent 6e2290b commit c6ef506
Show file tree
Hide file tree
Showing 4 changed files with 21 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ repos:
- flake8-unused-arguments==0.0.12
- flake8-noqa==1.3
- pep8-naming==0.13.2
- flake8-pyproject==1.2.1
- flake8-pyproject==1.2.2
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v2.7.1"
hooks:
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog/2594.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Ensure that two parallel tox instance invocations on different tox environment targets will work by holding a file lock
onto the packaging operations (e.g., in bash ``tox4 r -e py311 &; tox4 r -e py310``) - by :user:`gaborbernat`.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
"pyproject-api>=1.2.1",
'tomli>=2.0.1; python_version < "3.11"',
"virtualenv>=20.17",
"filelock>=3.8.1",
'importlib-metadata>=5.1; python_version < "3.8"',
'typing-extensions>=4.4; python_version < "3.8"',
]
Expand All @@ -49,7 +50,6 @@ optional-dependencies.testing = [
"devpi-process>=0.3",
"diff-cover>=7.2",
"distlib>=0.3.6",
"filelock>=3.8",
"flaky>=3.7",
"hatch-vcs>=0.2",
"hatchling>=1.11.1",
Expand Down
22 changes: 17 additions & 5 deletions src/tox/tox_env/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from types import MethodType
from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, cast

from filelock import FileLock

from tox.config.main import Config
from tox.config.sets import EnvConfigSet

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


def _lock_method(lock: RLock, meth: Callable[..., Any]) -> Callable[..., Any]:
def _lock_method(thread_lock: RLock, file_lock: FileLock | None, meth: Callable[..., Any]) -> Callable[..., Any]:
def _func(*args: Any, **kwargs: Any) -> Any:
with lock:
return meth(*args, **kwargs)
with thread_lock:
if file_lock is not None and file_lock.is_locked is False: # file_lock is to lock from other tox processes
file_lock.acquire()
try:
return meth(*args, **kwargs)
finally:
if file_lock is not None:
file_lock.release()

return _func


class PackageToxEnv(ToxEnv, ABC):
def __init__(self, create_args: ToxEnvCreateArgs) -> None:
self._lock = RLock()
self._thread_lock = RLock()
self._file_lock: FileLock | None = None
super().__init__(create_args)
self._envs: set[str] = set()

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

def register_config(self) -> None:
super().register_config()
file_lock_path: Path = self.conf["env_dir"] / "file.lock"
self._file_lock = FileLock(file_lock_path)
file_lock_path.parent.mkdir(parents=True, exist_ok=True)
self.core.add_config(
keys=["package_root", "setupdir"],
of_type=Path,
Expand Down

0 comments on commit c6ef506

Please sign in to comment.