Skip to content

Commit

Permalink
Add a flag to allow aborting installation if the lock file is out of …
Browse files Browse the repository at this point in the history
…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 python-poetry#5003.
  • Loading branch information
ashokdelphia committed Jan 13, 2023
1 parent df9d3c9 commit c2c5f3a
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 1 deletion.
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions src/poetry/console/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ class InstallCommand(InstallerCommand):
" (<warning>Deprecated</warning>)"
),
),
option(
"check-lock",
None,
(
"Verify that the poetry.lock file"
" is consistent with `pyproject.toml`"
" before installation."
),
),
option(
"extras",
"E",
Expand Down Expand Up @@ -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())

Expand Down
8 changes: 8 additions & 0 deletions src/poetry/installation/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(

self._execute_operations = True
self._lock = False
self._check_lock = False

self._whitelist: list[NormalizedName] = []

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -264,6 +270,8 @@ def _do_install(self) -> int:
"Run `poetry lock [--no-update]` to fix it."
"</warning>"
)
if self._check_lock:
return 1

locker_extras = {
canonicalize_name(extra)
Expand Down
13 changes: 13 additions & 0 deletions tests/console/commands/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
):
Expand Down
39 changes: 38 additions & 1 deletion tests/installation/test_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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"
Expand Down Expand Up @@ -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")
Expand Down

0 comments on commit c2c5f3a

Please sign in to comment.