diff --git a/docs/cli.md b/docs/cli.md index b5913b69040..5a5fd878f7e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -625,8 +625,9 @@ As such, `exit` should be used to properly exit the shell and the virtual enviro ## check -The `check` command validates the structure of the `pyproject.toml` file -and returns a detailed report if there are any errors. +The `check` command validates the content of the `pyproject.toml` file +and its consistency with the `poetry.lock` file. +It returns a detailed report if there are any errors. {{% note %}} This command is also available as a pre-commit hook. See [pre-commit hooks]({{< relref "pre-commit-hooks#poetry-check">}}) for more information. @@ -636,6 +637,10 @@ This command is also available as a pre-commit hook. See [pre-commit hooks]({{< poetry check ``` +### Options + +* `--lock`: Verifies that `poetry.lock` exists for the current `pyproject.toml`. + ## search This command searches for packages on a remote index. @@ -659,7 +664,7 @@ poetry lock ### Options -* `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml` +* `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml`. (**Deprecated**) Use `poetry check --lock` instead. * `--no-update`: Do not update locked versions, only refresh lock file. ## version @@ -944,7 +949,7 @@ poetry self lock #### Options -* `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml` +* `--check`: Verify that `poetry.lock` is consistent with `pyproject.toml`. (**Deprecated**) * `--no-update`: Do not update locked versions, only refresh lock file. ### self show diff --git a/src/poetry/console/commands/check.py b/src/poetry/console/commands/check.py index 3ca831e7213..733f0a7c5d5 100644 --- a/src/poetry/console/commands/check.py +++ b/src/poetry/console/commands/check.py @@ -1,11 +1,27 @@ from __future__ import annotations +from cleo.helpers import option + from poetry.console.commands.command import Command class CheckCommand(Command): name = "check" - description = "Checks the validity of the pyproject.toml file." + description = ( + "Validates the content of the pyproject.toml file and its" + " consistency with the poetry.lock file." + ) + + options = [ + option( + "lock", + None, + ( + "Checks that poetry.lock exists for the current" + " version of pyproject.toml." + ), + ), + ] def validate_classifiers( self, project_classifiers: set[str] @@ -72,6 +88,15 @@ def handle(self) -> int: check_result["errors"].extend(errors) check_result["warnings"].extend(warnings) + # Verify that lock file is consistent + if self.option("lock") and not self.poetry.locker.is_locked(): + check_result["errors"] += ["poetry.lock was not found."] + if self.poetry.locker.is_locked() and not self.poetry.locker.is_fresh(): + check_result["errors"] += [ + "poetry.lock is not consistent with pyproject.toml. Run `poetry" + " lock [--no-update]` to fix it." + ] + if not check_result["errors"] and not check_result["warnings"]: self.info("All set!") diff --git a/src/poetry/console/commands/lock.py b/src/poetry/console/commands/lock.py index c87f1359dc7..87273a66776 100644 --- a/src/poetry/console/commands/lock.py +++ b/src/poetry/console/commands/lock.py @@ -18,7 +18,8 @@ class LockCommand(InstallerCommand): None, ( "Check that the poetry.lock file corresponds to the current" - " version of pyproject.toml." + " version of pyproject.toml. (Deprecated) Use" + " poetry check --lock instead." ), ), ] @@ -36,6 +37,10 @@ class LockCommand(InstallerCommand): def handle(self) -> int: if self.option("check"): + self.line_error( + "poetry lock --check is deprecated, use `poetry" + " check --lock` instead." + ) if self.poetry.locker.is_locked() and self.poetry.locker.is_fresh(): self.line("poetry.lock is consistent with pyproject.toml.") return 0 diff --git a/tests/console/commands/test_check.py b/tests/console/commands/test_check.py index 323364c8def..6347d4585fb 100644 --- a/tests/console/commands/test_check.py +++ b/tests/console/commands/test_check.py @@ -4,13 +4,19 @@ import pytest +from poetry.packages import Locker + if TYPE_CHECKING: + import httpretty + from cleo.testers.command_tester import CommandTester from pytest_mock import MockerFixture + from poetry.poetry import Poetry from tests.types import CommandTesterFactory from tests.types import FixtureDirGetter + from tests.types import ProjectFactory @pytest.fixture() @@ -18,6 +24,36 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: return command_tester_factory("check") +def _project_factory( + fixture_name: str, + project_factory: ProjectFactory, + fixture_dir: FixtureDirGetter, +) -> Poetry: + source = fixture_dir(fixture_name) + pyproject_content = (source / "pyproject.toml").read_text(encoding="utf-8") + poetry_lock_content = (source / "poetry.lock").read_text(encoding="utf-8") + return project_factory( + name="foobar", + pyproject_content=pyproject_content, + poetry_lock_content=poetry_lock_content, + source=source, + ) + + +@pytest.fixture +def poetry_with_outdated_lockfile( + project_factory: ProjectFactory, fixture_dir: FixtureDirGetter +) -> Poetry: + return _project_factory("outdated_lock", project_factory, fixture_dir) + + +@pytest.fixture +def poetry_with_up_to_date_lockfile( + project_factory: ProjectFactory, fixture_dir: FixtureDirGetter +) -> Poetry: + return _project_factory("up_to_date_lock", project_factory, fixture_dir) + + def test_check_valid(tester: CommandTester) -> None: tester.execute() @@ -39,12 +75,13 @@ def test_check_invalid( new_callable=mocker.PropertyMock, ) - tester.execute() + tester.execute("--lock") expected = """\ Error: 'description' is a required property Error: Project name (invalid) is same as one of its dependencies Error: Unrecognized classifiers: ['Intended Audience :: Clowns']. +Error: poetry.lock was not found. Warning: A wildcard Python dependency is ambiguous.\ Consider specifying a more explicit one. Warning: The "pendulum" dependency specifies the "allows-prereleases" property,\ @@ -74,3 +111,88 @@ def test_check_private( """ assert tester.io.fetch_output() == expected + + +@pytest.mark.parametrize( + ("options", "expected", "expected_status"), + [ + ("", "All set!\n", 0), + ("--lock", "Error: poetry.lock was not found.\n", 1), + ], +) +def test_check_lock_missing( + mocker: MockerFixture, + tester: CommandTester, + fixture_dir: FixtureDirGetter, + options: str, + expected: str, + expected_status: int, +) -> None: + from poetry.toml import TOMLFile + + mocker.patch( + "poetry.poetry.Poetry.file", + return_value=TOMLFile(fixture_dir("private_pyproject") / "pyproject.toml"), + new_callable=mocker.PropertyMock, + ) + + status_code = tester.execute(options) + + assert status_code == expected_status + + if status_code == 0: + assert tester.io.fetch_output() == expected + else: + assert tester.io.fetch_error() == expected + + +@pytest.mark.parametrize("options", ["", "--lock"]) +def test_check_lock_outdated( + command_tester_factory: CommandTesterFactory, + poetry_with_outdated_lockfile: Poetry, + http: type[httpretty.httpretty], + options: str, +) -> None: + http.disable() + + locker = Locker( + lock=poetry_with_outdated_lockfile.pyproject.file.path.parent / "poetry.lock", + local_config=poetry_with_outdated_lockfile.locker._local_config, + ) + poetry_with_outdated_lockfile.set_locker(locker) + + tester = command_tester_factory("check", poetry=poetry_with_outdated_lockfile) + status_code = tester.execute(options) + expected = ( + "Error: poetry.lock is not consistent with pyproject.toml. " + "Run `poetry lock [--no-update]` to fix it.\n" + ) + + assert tester.io.fetch_error() == expected + + # exit with an error + assert status_code == 1 + + +@pytest.mark.parametrize("options", ["", "--lock"]) +def test_check_lock_up_to_date( + command_tester_factory: CommandTesterFactory, + poetry_with_up_to_date_lockfile: Poetry, + http: type[httpretty.httpretty], + options: str, +) -> None: + http.disable() + + locker = Locker( + lock=poetry_with_up_to_date_lockfile.pyproject.file.path.parent / "poetry.lock", + local_config=poetry_with_up_to_date_lockfile.locker._local_config, + ) + poetry_with_up_to_date_lockfile.set_locker(locker) + + tester = command_tester_factory("check", poetry=poetry_with_up_to_date_lockfile) + status_code = tester.execute(options) + expected = "All set!\n" + assert tester.io.fetch_output() == expected + + # exit with an error + assert status_code == 0 diff --git a/tests/console/commands/test_lock.py b/tests/console/commands/test_lock.py index 7a910caccf7..26e0b98277c 100644 --- a/tests/console/commands/test_lock.py +++ b/tests/console/commands/test_lock.py @@ -89,7 +89,7 @@ def poetry_with_invalid_lockfile( return _project_factory("invalid_lock", project_factory, fixture_dir) -def test_lock_check_outdated( +def test_lock_check_outdated_legacy( command_tester_factory: CommandTesterFactory, poetry_with_outdated_lockfile: Poetry, http: type[httpretty.httpretty], @@ -105,6 +105,7 @@ def test_lock_check_outdated( tester = command_tester_factory("lock", poetry=poetry_with_outdated_lockfile) status_code = tester.execute("--check") expected = ( + "poetry lock --check is deprecated, use `poetry check --lock` instead.\n" "Error: poetry.lock is not consistent with pyproject.toml. " "Run `poetry lock [--no-update]` to fix it.\n" ) @@ -115,7 +116,7 @@ def test_lock_check_outdated( assert status_code == 1 -def test_lock_check_up_to_date( +def test_lock_check_up_to_date_legacy( command_tester_factory: CommandTesterFactory, poetry_with_up_to_date_lockfile: Poetry, http: type[httpretty.httpretty], @@ -133,6 +134,11 @@ def test_lock_check_up_to_date( expected = "poetry.lock is consistent with pyproject.toml.\n" assert tester.io.fetch_output() == expected + expected_error = ( + "poetry lock --check is deprecated, use `poetry check --lock` instead.\n" + ) + assert tester.io.fetch_error() == expected_error + # exit with an error assert status_code == 0