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])