diff --git a/docs/cli.md b/docs/cli.md
index fb94f416ce1..404ff69f452 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -272,6 +272,13 @@ 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" >}}).
+
+
## 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 a10b41a309e..9a487df82a6 100644
--- a/src/poetry/console/application.py
+++ b/src/poetry/console/application.py
@@ -64,6 +64,7 @@ def _load() -> Command:
"search",
"shell",
"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..ed1610cc5b7
--- /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
+def command() -> str:
+ return "sync"
+
+
+@pytest.mark.skip("Only relevant for `poetry install`")
+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")