diff --git a/bugwarrior/command.py b/bugwarrior/command.py index bb591822..b83c58b0 100644 --- a/bugwarrior/command.py +++ b/bugwarrior/command.py @@ -8,8 +8,7 @@ import warnings import click -from lockfile import LockTimeout -from lockfile.pidlockfile import PIDLockFile +from filelock import FileLock, Timeout from bugwarrior.collect import aggregate_issues from bugwarrior.config import get_config_path, get_keyring, load_config @@ -123,21 +122,17 @@ def pull( config = _try_load_config(main_section, quiet) lockfile_path = os.path.join(config.main.data.path, 'bugwarrior.lockfile') - lockfile = PIDLockFile(lockfile_path) - lockfile.acquire(timeout=10) - try: + with FileLock(lockfile_path, timeout=10): # Get all the issues. This can take a while. issue_generator = aggregate_issues(config, debug) # Stuff them in the taskwarrior db as necessary synchronize(issue_generator, config, dry_run) - finally: - lockfile.release() - except LockTimeout: + except Timeout: log.critical( 'Your taskrc repository is currently locked. ' - 'Remove the file at %s if you are sure no other ' - 'bugwarrior processes are currently running.' % (lockfile_path) + 'Wait for any running bugwarrior processes to finish and try again. ' + f'Lock file:{lockfile_path}' ) sys.exit(1) except RuntimeError as e: diff --git a/bugwarrior/config/data.py b/bugwarrior/config/data.py index c9b30682..90e8fbe3 100644 --- a/bugwarrior/config/data.py +++ b/bugwarrior/config/data.py @@ -4,7 +4,7 @@ import subprocess import typing -from lockfile.pidlockfile import PIDLockFile +from filelock import FileLock def get_data_path(taskrc: str | Path) -> str: @@ -65,7 +65,7 @@ def get(self, key: str) -> typing.Any: def set(self, key: str, value: typing.Any) -> None: """Set a value in the ``bugwarrior.data`` file.""" - with PIDLockFile(self._lockfile): + with FileLock(self._lockfile): try: data = self.get_data() except OSError: # File does not exist. diff --git a/pyproject.toml b/pyproject.toml index 50250656..4c2b2eba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "click", "dogpile.cache>=0.5.3", "jinja2>=2.7.2", - "lockfile>=0.9.1", + "filelock>=3", "pydantic[email]>=2", "python-dateutil", "requests>=1", diff --git a/tests/test_command.py b/tests/test_command.py index cc6aa52b..53d2565c 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -173,6 +173,26 @@ def test_partial_failure_database_integrity(self, bugzillalib): self.assertNotIn('Closing 1 tasks', logs) self.assertNotIn('Completing task', logs) + @mock.patch('bugwarrior.command.FileLock') + def test_locked_repository(self, file_lock): + """ + A locked task repository should abort the pull. + """ + lockfile_path = pathlib.Path(self.lists_path) / 'bugwarrior.lockfile' + file_lock.return_value.__enter__.side_effect = command.Timeout( + str(lockfile_path) + ) + + with self.caplog.at_level(logging.CRITICAL): + result = self.runner.invoke(command.cli, args=('pull', '--debug')) + + self.assertEqual(result.exit_code, 1) + file_lock.assert_called_once_with(str(lockfile_path), timeout=10) + logs = [rec.message for rec in self.caplog.records] + self.assertTrue( + any('Your taskrc repository is currently locked.' in log for log in logs) + ) + @mock.patch('bugwarrior.services.github.GithubService.issues', fake_github_issues) def test_legacy_cli(self): """