From c2c5f3a7b066623b51f17f528f712410ad87102a Mon Sep 17 00:00:00 2001 From: Ashok Argent-Katwala Date: Fri, 13 Jan 2023 09:44:21 -0500 Subject: [PATCH] Add a flag to allow aborting installation if the lock file is out of sync. I'd like to ensure that when I'm using 'poetry install' in a CI pipeline that the lock file is up to date. This is possible at present by using: poetry lock --check && poetry install ... But I think it's a common enough use that providing it within the install command is still useful. Fixes #5003. --- docs/cli.md | 1 + src/poetry/console/commands/install.py | 10 +++++++ src/poetry/installation/installer.py | 8 ++++++ tests/console/commands/test_install.py | 13 +++++++++ tests/installation/test_installer.py | 39 +++++++++++++++++++++++++- 5 files changed, 70 insertions(+), 1 deletion(-) diff --git a/docs/cli.md b/docs/cli.md index 9a71e0e1b92..179a53127bb 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -234,6 +234,7 @@ poetry install --no-root * `--sync`: Synchronize the environment with the locked packages and the specified groups. * `--no-root`: Do not install the root package (your project). * `--dry-run`: Output the operations but do not execute anything (implicitly enables --verbose). +* `--check-lock`: Verify that `poetry.lock` is consistent with `pyproject.toml` before installation. * `--extras (-E)`: Features to install (multiple values allowed). * `--all-extras`: Install all extra features (conflicts with --extras). * `--no-dev`: Do not install dev dependencies. (**Deprecated**, use `--without dev` or `--only main` instead) diff --git a/src/poetry/console/commands/install.py b/src/poetry/console/commands/install.py index b499970aac9..9b70fe73b43 100644 --- a/src/poetry/console/commands/install.py +++ b/src/poetry/console/commands/install.py @@ -46,6 +46,15 @@ class InstallCommand(InstallerCommand): " (Deprecated)" ), ), + option( + "check-lock", + None, + ( + "Verify that the poetry.lock file" + " is consistent with `pyproject.toml`" + " before installation." + ), + ), option( "extras", "E", @@ -145,6 +154,7 @@ def handle(self) -> int: self.installer.only_groups(self.activated_groups) self.installer.dry_run(self.option("dry-run")) + self.installer.check_lock(self.option("check-lock")) self.installer.requires_synchronization(with_synchronization) self.installer.verbose(self.io.is_verbose()) diff --git a/src/poetry/installation/installer.py b/src/poetry/installation/installer.py index 9b906f7678f..1007fe7f601 100644 --- a/src/poetry/installation/installer.py +++ b/src/poetry/installation/installer.py @@ -60,6 +60,7 @@ def __init__( self._execute_operations = True self._lock = False + self._check_lock = False self._whitelist: list[NormalizedName] = [] @@ -113,6 +114,11 @@ def run(self) -> int: return self._do_install() + def check_lock(self, check_lock: bool) -> Installer: + self._check_lock = check_lock + + return self + def dry_run(self, dry_run: bool = True) -> Installer: self._dry_run = dry_run self._executor.dry_run(dry_run) @@ -264,6 +270,8 @@ def _do_install(self) -> int: "Run `poetry lock [--no-update]` to fix it." "" ) + if self._check_lock: + return 1 locker_extras = { canonicalize_name(extra) diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index a2bda6a4651..5a5d3af284f 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -145,6 +145,19 @@ def test_sync_option_is_passed_to_the_installer( assert tester.command.installer._requires_synchronization +def test_check_lock_option_is_passed_to_the_installer( + tester: CommandTester, mocker: MockerFixture +): + """ + The --check-lock option is passed properly to the installer. + """ + mocker.patch.object(tester.command.installer, "run", return_value=1) + + tester.execute("--check-lock") + + assert tester.command.installer._check_lock + + def test_no_all_extras_doesnt_populate_installer( tester: CommandTester, mocker: MockerFixture ): diff --git a/tests/installation/test_installer.py b/tests/installation/test_installer.py index 70761e48ebf..a95f4f7829d 100644 --- a/tests/installation/test_installer.py +++ b/tests/installation/test_installer.py @@ -105,6 +105,7 @@ def __init__(self, lock_path: str | Path) -> None: self._lock = TOMLFile(Path(lock_path).joinpath("poetry.lock")) self._written_data = None self._locked = False + self._fresh = True self._content_hash = self._get_content_hash() @property @@ -116,6 +117,11 @@ def set_lock_path(self, lock: str | Path) -> Locker: return self + def fresh(self, is_fresh: bool = True) -> Locker: + self._fresh = is_fresh + + return self + def locked(self, is_locked: bool = True) -> Locker: self._locked = is_locked @@ -128,7 +134,7 @@ def is_locked(self) -> bool: return self._locked def is_fresh(self) -> bool: - return True + return self._fresh def _get_content_hash(self) -> str: return "123456789" @@ -208,6 +214,37 @@ def fixture(name: str) -> dict: return json.loads(json.dumps(file.read())) +def test_run_check_lock(installer: Installer, locker: Locker): + locker.fresh(False) + locker.locked(True) + locker.mock_lock_data( + { + "package": [ + { + "name": "A", + "version": "1.0", + "category": "main", + "optional": False, + "platform": "*", + "python-versions": "*", + "checksum": [], + }, + ], + "metadata": { + "python-versions": "*", + "platform": "*", + "content-hash": "123456789", + "files": {"A": []}, + }, + } + ) + installer.check_lock(True) + + return_code = installer.run() + + assert return_code == 1 + + def test_run_no_dependencies(installer: Installer, locker: Locker): installer.run() expected = fixture("no-dependencies")