restore wrapper around filelock

This commit is contained in:
2026-02-14 15:04:06 +02:00
parent 872a119bea
commit d26525d1d3
3 changed files with 50 additions and 6 deletions

View File

@@ -20,7 +20,6 @@
import shutil import shutil
from collections.abc import Iterable from collections.abc import Iterable
from filelock import FileLock
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@@ -28,7 +27,7 @@ from ahriman.core.build_tools.package_archive import PackageArchive
from ahriman.core.build_tools.task import Task from ahriman.core.build_tools.task import Task
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.repository.package_info import PackageInfo from ahriman.core.repository.package_info import PackageInfo
from ahriman.core.utils import atomic_move, list_flatmap, package_like, safe_filename, symlink_relative from ahriman.core.utils import atomic_move, filelock, list_flatmap, package_like, safe_filename, symlink_relative
from ahriman.models.changes import Changes from ahriman.models.changes import Changes
from ahriman.models.event import EventType from ahriman.models.event import EventType
from ahriman.models.package import Package from ahriman.models.package import Package
@@ -112,7 +111,7 @@ class Executor(PackageInfo, Cleaner):
self.logger.info("using prebuilt packages for %s-%s", loaded_package.base, loaded_package.version) self.logger.info("using prebuilt packages for %s-%s", loaded_package.base, loaded_package.version)
built = [] built = []
for artifact in prebuilt: for artifact in prebuilt:
with FileLock(artifact.with_name(f".{artifact.name}.lock")): with filelock(artifact):
shutil.copy(artifact, path) shutil.copy(artifact, path)
built.append(path / artifact.name) built.append(path / artifact.name)
else: else:

View File

@@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
import contextlib
import datetime import datetime
import io import io
import itertools import itertools
@@ -47,6 +48,7 @@ __all__ = [
"dataclass_view", "dataclass_view",
"enum_values", "enum_values",
"extract_user", "extract_user",
"filelock",
"filter_json", "filter_json",
"full_version", "full_version",
"list_flatmap", "list_flatmap",
@@ -87,7 +89,7 @@ def atomic_move(src: Path, dst: Path) -> None:
>>> atomic_move(src, dst) >>> atomic_move(src, dst)
""" """
with FileLock(dst.with_name(f".{dst.name}.lock")): with filelock(dst):
shutil.move(src, dst) shutil.move(src, dst)
@@ -262,6 +264,25 @@ def extract_user() -> str | None:
return os.getenv("SUDO_USER") or os.getenv("DOAS_USER") or os.getenv("USER") return os.getenv("SUDO_USER") or os.getenv("DOAS_USER") or os.getenv("USER")
@contextlib.contextmanager
def filelock(path: Path) -> Iterator[FileLock]:
"""
wrapper around :class:`filelock.FileLock`, which also removes locks afterward
Args:
path(Path): path to lock on. The lock file will be created as ``.{path.name}.lock``
Yields:
FileLock: acquired file lock instance
"""
lock_path = path.with_name(f".{path.name}.lock")
try:
with FileLock(lock_path) as lock:
yield lock
finally:
lock_path.unlink(missing_ok=True)
def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str, Any]: def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str, Any]:
""" """
filter json object by fields used for json-to-object conversion filter json object by fields used for json-to-object conversion

View File

@@ -20,11 +20,11 @@ def test_atomic_move(mocker: MockerFixture) -> None:
""" """
must move file with locking must move file with locking
""" """
filelock_mock = mocker.patch("ahriman.core.utils.FileLock") filelock_mock = mocker.patch("ahriman.core.utils.filelock")
move_mock = mocker.patch("shutil.move") move_mock = mocker.patch("shutil.move")
atomic_move(Path("source"), Path("destination")) atomic_move(Path("source"), Path("destination"))
filelock_mock.assert_called_once_with(Path(".destination.lock")) filelock_mock.assert_called_once_with(Path("destination"))
move_mock.assert_called_once_with(Path("source"), Path("destination")) move_mock.assert_called_once_with(Path("source"), Path("destination"))
@@ -247,6 +247,30 @@ def test_extract_user() -> None:
assert extract_user() == "doas" assert extract_user() == "doas"
def test_filelock(tmp_path: Path) -> None:
"""
must acquire lock and remove lock file after
"""
local = tmp_path / "local"
lock = local.with_name(f".{local.name}.lock")
with filelock(local):
assert lock.exists()
assert not lock.exists()
def test_filelock_cleanup_on_missing(tmp_path: Path) -> None:
"""
must not fail if lock file is already removed
"""
local = tmp_path / "local"
lock = local.with_name(f".{local.name}.lock")
with filelock(local):
lock.unlink(missing_ok=True)
assert not lock.exists()
def test_filter_json(package_ahriman: Package) -> None: def test_filter_json(package_ahriman: Package) -> None:
""" """
must filter fields by known list must filter fields by known list