From e9673f0b2d4e651c803f2aa13ab063145440bc77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Sun, 27 Oct 2024 15:19:20 +0100 Subject: [PATCH 1/2] introduce `sync` command --- docs/cli.md | 15 +++++++++++ src/poetry/console/application.py | 1 + src/poetry/console/commands/sync.py | 36 ++++++++++++++++++++++++++ tests/console/commands/test_install.py | 24 ++++++++++++----- tests/console/commands/test_sync.py | 30 +++++++++++++++++++++ 5 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 src/poetry/console/commands/sync.py create mode 100644 tests/console/commands/test_sync.py diff --git a/docs/cli.md b/docs/cli.md index 879664918fe..bf323592676 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -272,6 +272,21 @@ When `--only` is specified, `--with` and `--without` options are ignored. {{% /note %}} +## sync + +The `sync` command makes sure that the project's environment is in sync with the `poetry.lock` file. +It is equivalent to running `poetry install --sync` and provides the same options +(except for `--sync`) as [install]({{< relref "#install" >}}). + +{{% note %}} +Normally, you should prefer `poetry sync` to `poetry install` to avoid untracked outdated packages. +However, if you have set `virtualenvs.create = false` to install dependencies into your system environment, +which is discouraged, or `virtualenvs.options.system-site-packages = true` to make +system site-packages available in your virtual environment, you should use `poetry install` +because `poetry sync` will normally not work well in these cases. +{{% /note %}} + + ## update In order to get the latest versions of the dependencies and to update the `poetry.lock` file, diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index e2d79c449ae..91633c58d42 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -63,6 +63,7 @@ def _load() -> Command: "run", "search", "show", + "sync", "update", "version", # Cache commands diff --git a/src/poetry/console/commands/sync.py b/src/poetry/console/commands/sync.py new file mode 100644 index 00000000000..29d6c1be873 --- /dev/null +++ b/src/poetry/console/commands/sync.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import ClassVar + +from poetry.console.commands.install import InstallCommand + + +if TYPE_CHECKING: + from cleo.io.inputs.option import Option + + +class SyncCommand(InstallCommand): + name = "sync" + description = "Update the project's environment according to the lockfile." + + options: ClassVar[list[Option]] = [ + opt for opt in InstallCommand.options if opt.name != "sync" + ] + + help = """\ +The sync command makes sure that the project's environment is in sync with +the poetry.lock file. +It is equivalent to running poetry install --sync. + +poetry sync + +By default, the above command will also install the current project. To install only the +dependencies and not including the current project, run the command with the +--no-root option like below: + + poetry sync --no-root + +If you want to use Poetry only for dependency management but not for packaging, +you can set the "package-mode" to false in your pyproject.toml file. +""" diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index ae163df3ec2..15125be4a23 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -63,6 +63,11 @@ """ +@pytest.fixture +def command() -> str: + return "install" + + @pytest.fixture def poetry(project_factory: ProjectFactory) -> Poetry: return project_factory(name="export", pyproject_content=PYPROJECT_CONTENT) @@ -70,9 +75,9 @@ def poetry(project_factory: ProjectFactory) -> Poetry: @pytest.fixture def tester( - command_tester_factory: CommandTesterFactory, poetry: Poetry + command_tester_factory: CommandTesterFactory, command: str, poetry: Poetry ) -> CommandTester: - return command_tester_factory("install") + return command_tester_factory(command) def _project_factory( @@ -443,6 +448,7 @@ def test_install_logs_output_decorated( @pytest.mark.parametrize("error", ["module", "readme", ""]) def test_install_warning_corrupt_root( command_tester_factory: CommandTesterFactory, + command: str, project_factory: ProjectFactory, with_root: bool, error: str, @@ -461,7 +467,7 @@ def test_install_warning_corrupt_root( if error != "module": (poetry.pyproject_path.parent / f"{name}.py").touch() - tester = command_tester_factory("install", poetry=poetry) + tester = command_tester_factory(command, poetry=poetry) tester.execute("" if with_root else "--no-root") if error and with_root: @@ -481,6 +487,7 @@ def test_install_warning_corrupt_root( ) def test_install_path_dependency_does_not_exist( command_tester_factory: CommandTesterFactory, + command: str, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, project: str, @@ -489,7 +496,7 @@ def test_install_path_dependency_does_not_exist( poetry = _project_factory(project, project_factory, fixture_dir) assert isinstance(poetry.locker, TestLocker) poetry.locker.locked(True) - tester = command_tester_factory("install", poetry=poetry) + tester = command_tester_factory(command, poetry=poetry) if options: tester.execute(options) else: @@ -500,6 +507,7 @@ def test_install_path_dependency_does_not_exist( @pytest.mark.parametrize("options", ["", "--extras notinstallable"]) def test_install_extra_path_dependency_does_not_exist( command_tester_factory: CommandTesterFactory, + command: str, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, options: str, @@ -508,7 +516,7 @@ def test_install_extra_path_dependency_does_not_exist( poetry = _project_factory(project, project_factory, fixture_dir) assert isinstance(poetry.locker, TestLocker) poetry.locker.locked(True) - tester = command_tester_factory("install", poetry=poetry) + tester = command_tester_factory(command, poetry=poetry) if not options: tester.execute(options) else: @@ -519,6 +527,7 @@ def test_install_extra_path_dependency_does_not_exist( @pytest.mark.parametrize("options", ["", "--no-directory"]) def test_install_missing_directory_dependency_with_no_directory( command_tester_factory: CommandTesterFactory, + command: str, project_factory: ProjectFactory, fixture_dir: FixtureDirGetter, options: str, @@ -528,7 +537,7 @@ def test_install_missing_directory_dependency_with_no_directory( ) assert isinstance(poetry.locker, TestLocker) poetry.locker.locked(True) - tester = command_tester_factory("install", poetry=poetry) + tester = command_tester_factory(command, poetry=poetry) if options: tester.execute(options) else: @@ -538,6 +547,7 @@ def test_install_missing_directory_dependency_with_no_directory( def test_non_package_mode_does_not_try_to_install_root( command_tester_factory: CommandTesterFactory, + command: str, project_factory: ProjectFactory, ) -> None: content = """\ @@ -546,7 +556,7 @@ def test_non_package_mode_does_not_try_to_install_root( """ poetry = project_factory(name="non-package-mode", pyproject_content=content) - tester = command_tester_factory("install", poetry=poetry) + tester = command_tester_factory(command, poetry=poetry) tester.execute() assert tester.status_code == 0 diff --git a/tests/console/commands/test_sync.py b/tests/console/commands/test_sync.py new file mode 100644 index 00000000000..af75afd86f3 --- /dev/null +++ b/tests/console/commands/test_sync.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from cleo.exceptions import CleoNoSuchOptionError + +# import all tests from the install command +# and run them for sync by overriding the command fixture +from tests.console.commands.test_install import * # noqa: F403 + + +if TYPE_CHECKING: + from cleo.testers.command_tester import CommandTester + + +@pytest.fixture # type: ignore[no-redef] +def command() -> str: + return "sync" + + +@pytest.mark.skip("Only relevant for `poetry install`") # type: ignore[no-redef] +def test_sync_option_is_passed_to_the_installer() -> None: + """The only test from the install command that does not work for sync.""" + + +def test_sync_option_not_available(tester: CommandTester) -> None: + with pytest.raises(CleoNoSuchOptionError): + tester.execute("--sync") From 2812a1a231d8655a54cfbc0d911bc0eb9115a0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Wed, 1 Jan 2025 07:46:07 +0100 Subject: [PATCH 2/2] deprecate `install --sync` and introduce `self sync command` for symmetry --- docs/cli.md | 169 ++++++++++++++++---- src/poetry/console/application.py | 1 + src/poetry/console/commands/install.py | 14 +- src/poetry/console/commands/self/install.py | 4 + src/poetry/console/commands/self/sync.py | 31 ++++ tests/console/commands/self/test_install.py | 21 ++- tests/console/commands/self/test_sync.py | 30 ++++ tests/console/commands/test_install.py | 14 +- 8 files changed, 251 insertions(+), 33 deletions(-) create mode 100644 src/poetry/console/commands/self/sync.py create mode 100644 tests/console/commands/self/test_sync.py diff --git a/docs/cli.md b/docs/cli.md index bf323592676..dc25f441040 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -142,6 +142,14 @@ poetry init The `install` command reads the `pyproject.toml` file from the current project, resolves the dependencies, and installs them. +{{% note %}} +Normally, you should prefer `poetry sync` to `poetry install` to avoid untracked outdated packages. +However, if you have set `virtualenvs.create = false` to install dependencies into your system environment, +which is discouraged, or `virtualenvs.options.system-site-packages = true` to make +system site-packages available in your virtual environment, you should use `poetry install` +because `poetry sync` will normally not work well in these cases. +{{% /note %}} + ```bash poetry install ``` @@ -186,21 +194,6 @@ poetry install --only-root See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) for more information about dependency groups. -If you want to synchronize your environment – and ensure it matches the lock file – use the -`--sync` option. - -```bash -poetry install --sync -``` - -The `--sync` can be combined with group-related options: - -```bash -poetry install --without dev --sync -poetry install --with docs --sync -poetry install --only dev --sync -``` - You can also specify the extras you want installed by passing the `-E|--extras` option (See [Extras]({{< relref "pyproject#extras" >}}) for more info). Pass `--all-extras` to install all defined extras for a project. @@ -211,7 +204,7 @@ poetry install -E mysql -E pgsql poetry install --all-extras ``` -Extras are not sensitive to `--sync`. Any extras not specified will always be removed. +Any extras not specified will always be removed. ```bash poetry install --extras "A B" # C is removed @@ -258,7 +251,7 @@ poetry install --compile * `--with`: The optional dependency groups to include. * `--only`: The only dependency groups to include. * `--only-root`: Install only the root project, exclude all dependencies. -* `--sync`: Synchronize the environment with the locked packages and the specified groups. +* `--sync`: Synchronize the environment with the locked packages and the specified groups. (**Deprecated**, use `poetry sync` instead) * `--no-root`: Do not install the root package (your project). * `--no-directory`: Skip all directory path dependencies (including transitive ones). * `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). @@ -275,15 +268,119 @@ When `--only` is specified, `--with` and `--without` options are ignored. ## sync The `sync` command makes sure that the project's environment is in sync with the `poetry.lock` file. -It is equivalent to running `poetry install --sync` and provides the same options -(except for `--sync`) as [install]({{< relref "#install" >}}). +It is similar to `poetry install` but it additionally removes packages that are not tracked in the lock file. + +```bash +poetry sync +``` + +If there is a `poetry.lock` file in the current directory, +it will use the exact versions from there instead of resolving them. +This ensures that everyone using the library will get the same versions of the dependencies. + +If there is no `poetry.lock` file, Poetry will create one after dependency resolution. + +If you want to exclude one or more dependency groups for the installation, you can use +the `--without` option. + +```bash +poetry sync --without test,docs +``` + +You can also select optional dependency groups with the `--with` option. + +```bash +poetry sync --with test,docs +``` + +To install all dependency groups including the optional groups, use the ``--all-groups`` flag. + +```bash +poetry sync --all-groups +``` + +It's also possible to only install specific dependency groups by using the `only` option. + +```bash +poetry sync --only test,docs +``` + +To only install the project itself with no dependencies, use the `--only-root` flag. + +```bash +poetry sync --only-root +``` + +See [Dependency groups]({{< relref "managing-dependencies#dependency-groups" >}}) for more information +about dependency groups. + +You can also specify the extras you want installed +by passing the `-E|--extras` option (See [Extras]({{< relref "pyproject#extras" >}}) for more info). +Pass `--all-extras` to install all defined extras for a project. + +```bash +poetry sync --extras "mysql pgsql" +poetry sync -E mysql -E pgsql +poetry sync --all-extras +``` + +Any extras not specified will always be removed. + +```bash +poetry sync --extras "A B" # C is removed +``` + +By default `poetry` will install your project's package every time you run `sync`: + +```bash +$ poetry sync +Installing dependencies from lock file + +No dependencies to install or update + + - Installing (x.x.x) +``` + +If you want to skip this installation, use the `--no-root` option. + +```bash +poetry sync --no-root +``` + +Similar to `--no-root` you can use `--no-directory` to skip directory path dependencies: + +```bash +poetry sync --no-directory +``` + +This is mainly useful for caching in CI or when building Docker images. See the [FAQ entry]({{< relref "faq#poetry-busts-my-docker-cache-because-it-requires-me-to-copy-my-source-files-in-before-installing-3rd-party-dependencies" >}}) for more information on this option. + +By default `poetry` does not compile Python source files to bytecode during installation. +This speeds up the installation process, but the first execution may take a little more +time because Python then compiles source files to bytecode automatically. +If you want to compile source files to bytecode during installation, +you can use the `--compile` option: + +```bash +poetry sync --compile +``` + +### Options + +* `--without`: The dependency groups to ignore. +* `--with`: The optional dependency groups to include. +* `--only`: The only dependency groups to include. +* `--only-root`: Install only the root project, exclude all dependencies. +* `--no-root`: Do not install the root package (your project). +* `--no-directory`: Skip all directory path dependencies (including transitive ones). +* `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). +* `--extras (-E)`: Features to install (multiple values allowed). +* `--all-extras`: Install all extra features (conflicts with `--extras`). +* `--all-groups`: Install dependencies from all groups (conflicts with `--only`, `--with`, and `--without`). +* `--compile`: Compile Python source files to bytecode. {{% note %}} -Normally, you should prefer `poetry sync` to `poetry install` to avoid untracked outdated packages. -However, if you have set `virtualenvs.create = false` to install dependencies into your system environment, -which is discouraged, or `virtualenvs.options.system-site-packages = true` to make -system site-packages available in your virtual environment, you should use `poetry install` -because `poetry sync` will normally not work well in these cases. +When `--only` is specified, `--with` and `--without` options are ignored. {{% /note %}} @@ -1039,16 +1136,34 @@ runtime environment. {{% note %}} The `self install` command works similar to the [`install` command](#install). However, -is different in that the packages managed are for Poetry's runtime environment. +it is different in that the packages managed are for Poetry's runtime environment. {{% /note %}} ```bash -poetry self install --sync +poetry self install +``` + +#### Options + +* `--sync`: Synchronize the environment with the locked packages and the specified groups. (**Deprecated**, use `poetry self sync` instead) +* `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). + +### self sync + +The `self sync` command ensures all additional (and no other) packages specified +are installed in the current runtime environment. + +{{% note %}} +The `self sync` command works similar to the [`sync` command](#sync). However, +it is different in that the packages managed are for Poetry's runtime environment. +{{% /note %}} + +```bash +poetry self sync ``` #### Options -* `--sync`: Synchronize the environment with the locked packages and the specified groups. * `--dry-run`: Output the operations but do not execute anything (implicitly enables `--verbose`). ## export diff --git a/src/poetry/console/application.py b/src/poetry/console/application.py index 91633c58d42..dc17735770b 100644 --- a/src/poetry/console/application.py +++ b/src/poetry/console/application.py @@ -86,6 +86,7 @@ def _load() -> Command: "self update", "self show", "self show plugins", + "self sync", # Source commands "source add", "source remove", diff --git a/src/poetry/console/commands/install.py b/src/poetry/console/commands/install.py index 972d04d8f77..bedb292b587 100644 --- a/src/poetry/console/commands/install.py +++ b/src/poetry/console/commands/install.py @@ -23,7 +23,7 @@ class InstallCommand(InstallerCommand): "sync", None, "Synchronize the environment with the locked packages and the specified" - " groups.", + " groups. (Deprecated)", ), option( "no-root", None, "Do not install the root package (the current project)." @@ -89,6 +89,10 @@ def activated_groups(self) -> set[str]: else: return super().activated_groups + @property + def _alternative_sync_command(self) -> str: + return "poetry sync" + def handle(self) -> int: from poetry.core.masonry.utils.module import ModuleOrPackageNotFoundError @@ -147,6 +151,14 @@ def handle(self) -> int: self.installer.extras(extras) with_synchronization = self.option("sync") + if with_synchronization: + self.line_error( + "The `--sync` option is" + " deprecated and slated for removal in the next minor release" + " after June 2025, use the" + f" `{self._alternative_sync_command}`" + " command instead." + ) self.installer.only_groups(self.activated_groups) self.installer.skip_directory(self.option("no-directory")) diff --git a/src/poetry/console/commands/self/install.py b/src/poetry/console/commands/self/install.py index c0870b2280e..6f0399b2ae2 100644 --- a/src/poetry/console/commands/self/install.py +++ b/src/poetry/console/commands/self/install.py @@ -35,3 +35,7 @@ class SelfInstallCommand(SelfCommand, InstallCommand): @property def activated_groups(self) -> set[str]: return {MAIN_GROUP, self.default_group} + + @property + def _alternative_sync_command(self) -> str: + return "poetry self sync" diff --git a/src/poetry/console/commands/self/sync.py b/src/poetry/console/commands/self/sync.py new file mode 100644 index 00000000000..0af576c7b80 --- /dev/null +++ b/src/poetry/console/commands/self/sync.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import ClassVar + +from poetry.console.commands.self.install import SelfInstallCommand + + +if TYPE_CHECKING: + from cleo.io.inputs.option import Option + + +class SelfSyncCommand(SelfInstallCommand): + name = "self sync" + description = ( + "Sync Poetry's own environment according to the locked packages (incl. addons)" + " required by this Poetry installation." + ) + options: ClassVar[list[Option]] = [ + opt for opt in SelfInstallCommand.options if opt.name != "sync" + ] + help = f"""\ +The self sync command ensures all additional (and no other) packages \ +specified are installed in the current runtime environment. + +This is managed in the \ +{SelfInstallCommand.get_default_system_pyproject_file()} file. + +You can add more packages using the self add command and remove them using \ +the self remove command. +""" diff --git a/tests/console/commands/self/test_install.py b/tests/console/commands/self/test_install.py index b9cbbb59dc4..a5e15e41ed6 100644 --- a/tests/console/commands/self/test_install.py +++ b/tests/console/commands/self/test_install.py @@ -14,8 +14,13 @@ @pytest.fixture -def tester(command_tester_factory: CommandTesterFactory) -> CommandTester: - return command_tester_factory("self install") +def command() -> str: + return "self install" + + +@pytest.fixture +def tester(command_tester_factory: CommandTesterFactory, command: str) -> CommandTester: + return command_tester_factory(command) @pytest.mark.parametrize( @@ -60,3 +65,15 @@ def test_self_install( assert tester.io.fetch_output() == expected_output assert tester.io.fetch_error() == "" + + +@pytest.mark.parametrize("sync", [True, False]) +def test_sync_deprecation(tester: CommandTester, sync: bool) -> None: + tester.execute("--sync" if sync else "") + + error = tester.io.fetch_error() + if sync: + assert "deprecated" in error + assert "poetry self sync" in error + else: + assert error == "" diff --git a/tests/console/commands/self/test_sync.py b/tests/console/commands/self/test_sync.py new file mode 100644 index 00000000000..6fdc27fc764 --- /dev/null +++ b/tests/console/commands/self/test_sync.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from cleo.exceptions import CleoNoSuchOptionError + +# import all tests from the self install command +# and run them for sync by overriding the command fixture +from tests.console.commands.self.test_install import * # noqa: F403 + + +if TYPE_CHECKING: + from cleo.testers.command_tester import CommandTester + + +@pytest.fixture # type: ignore[no-redef] +def command() -> str: + return "self sync" + + +@pytest.mark.skip("Only relevant for `poetry self install`") # type: ignore[no-redef] +def test_sync_deprecation() -> None: + """The only test from the self install command that does not work for self sync.""" + + +def test_sync_option_not_available(tester: CommandTester) -> None: + with pytest.raises(CleoNoSuchOptionError): + tester.execute("--sync") diff --git a/tests/console/commands/test_install.py b/tests/console/commands/test_install.py index 15125be4a23..8d8d2620ded 100644 --- a/tests/console/commands/test_install.py +++ b/tests/console/commands/test_install.py @@ -159,8 +159,9 @@ def test_group_options_are_passed_to_the_installer( assert editable_builder_mock.call_count == 0 +@pytest.mark.parametrize("sync", [True, False]) def test_sync_option_is_passed_to_the_installer( - tester: CommandTester, mocker: MockerFixture + tester: CommandTester, mocker: MockerFixture, sync: bool ) -> None: """ The --sync option is passed properly to the installer. @@ -168,9 +169,16 @@ def test_sync_option_is_passed_to_the_installer( assert isinstance(tester.command, InstallerCommand) mocker.patch.object(tester.command.installer, "run", return_value=1) - tester.execute("--sync") + tester.execute("--sync" if sync else "") + + assert tester.command.installer._requires_synchronization is sync - assert tester.command.installer._requires_synchronization + error = tester.io.fetch_error() + if sync: + assert "deprecated" in error + assert "poetry sync" in error + else: + assert error == "" @pytest.mark.parametrize("compile", [False, True])