Skip to content

Commit e9673f0

Browse files
committed
introduce sync command
1 parent 2d540ea commit e9673f0

File tree

5 files changed

+99
-7
lines changed

5 files changed

+99
-7
lines changed

docs/cli.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,21 @@ When `--only` is specified, `--with` and `--without` options are ignored.
272272
{{% /note %}}
273273

274274

275+
## sync
276+
277+
The `sync` command makes sure that the project's environment is in sync with the `poetry.lock` file.
278+
It is equivalent to running `poetry install --sync` and provides the same options
279+
(except for `--sync`) as [install]({{< relref "#install" >}}).
280+
281+
{{% note %}}
282+
Normally, you should prefer `poetry sync` to `poetry install` to avoid untracked outdated packages.
283+
However, if you have set `virtualenvs.create = false` to install dependencies into your system environment,
284+
which is discouraged, or `virtualenvs.options.system-site-packages = true` to make
285+
system site-packages available in your virtual environment, you should use `poetry install`
286+
because `poetry sync` will normally not work well in these cases.
287+
{{% /note %}}
288+
289+
275290
## update
276291

277292
In order to get the latest versions of the dependencies and to update the `poetry.lock` file,

src/poetry/console/application.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def _load() -> Command:
6363
"run",
6464
"search",
6565
"show",
66+
"sync",
6667
"update",
6768
"version",
6869
# Cache commands

src/poetry/console/commands/sync.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
from typing import ClassVar
5+
6+
from poetry.console.commands.install import InstallCommand
7+
8+
9+
if TYPE_CHECKING:
10+
from cleo.io.inputs.option import Option
11+
12+
13+
class SyncCommand(InstallCommand):
14+
name = "sync"
15+
description = "Update the project's environment according to the lockfile."
16+
17+
options: ClassVar[list[Option]] = [
18+
opt for opt in InstallCommand.options if opt.name != "sync"
19+
]
20+
21+
help = """\
22+
The <info>sync</info> command makes sure that the project's environment is in sync with
23+
the <comment>poetry.lock</> file.
24+
It is equivalent to running <info>poetry install --sync</info>.
25+
26+
<info>poetry sync</info>
27+
28+
By default, the above command will also install the current project. To install only the
29+
dependencies and not including the current project, run the command with the
30+
<info>--no-root</info> option like below:
31+
32+
<info> poetry sync --no-root</info>
33+
34+
If you want to use Poetry only for dependency management but not for packaging,
35+
you can set the "package-mode" to false in your pyproject.toml file.
36+
"""

tests/console/commands/test_install.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,21 @@
6363
"""
6464

6565

66+
@pytest.fixture
67+
def command() -> str:
68+
return "install"
69+
70+
6671
@pytest.fixture
6772
def poetry(project_factory: ProjectFactory) -> Poetry:
6873
return project_factory(name="export", pyproject_content=PYPROJECT_CONTENT)
6974

7075

7176
@pytest.fixture
7277
def tester(
73-
command_tester_factory: CommandTesterFactory, poetry: Poetry
78+
command_tester_factory: CommandTesterFactory, command: str, poetry: Poetry
7479
) -> CommandTester:
75-
return command_tester_factory("install")
80+
return command_tester_factory(command)
7681

7782

7883
def _project_factory(
@@ -443,6 +448,7 @@ def test_install_logs_output_decorated(
443448
@pytest.mark.parametrize("error", ["module", "readme", ""])
444449
def test_install_warning_corrupt_root(
445450
command_tester_factory: CommandTesterFactory,
451+
command: str,
446452
project_factory: ProjectFactory,
447453
with_root: bool,
448454
error: str,
@@ -461,7 +467,7 @@ def test_install_warning_corrupt_root(
461467
if error != "module":
462468
(poetry.pyproject_path.parent / f"{name}.py").touch()
463469

464-
tester = command_tester_factory("install", poetry=poetry)
470+
tester = command_tester_factory(command, poetry=poetry)
465471
tester.execute("" if with_root else "--no-root")
466472

467473
if error and with_root:
@@ -481,6 +487,7 @@ def test_install_warning_corrupt_root(
481487
)
482488
def test_install_path_dependency_does_not_exist(
483489
command_tester_factory: CommandTesterFactory,
490+
command: str,
484491
project_factory: ProjectFactory,
485492
fixture_dir: FixtureDirGetter,
486493
project: str,
@@ -489,7 +496,7 @@ def test_install_path_dependency_does_not_exist(
489496
poetry = _project_factory(project, project_factory, fixture_dir)
490497
assert isinstance(poetry.locker, TestLocker)
491498
poetry.locker.locked(True)
492-
tester = command_tester_factory("install", poetry=poetry)
499+
tester = command_tester_factory(command, poetry=poetry)
493500
if options:
494501
tester.execute(options)
495502
else:
@@ -500,6 +507,7 @@ def test_install_path_dependency_does_not_exist(
500507
@pytest.mark.parametrize("options", ["", "--extras notinstallable"])
501508
def test_install_extra_path_dependency_does_not_exist(
502509
command_tester_factory: CommandTesterFactory,
510+
command: str,
503511
project_factory: ProjectFactory,
504512
fixture_dir: FixtureDirGetter,
505513
options: str,
@@ -508,7 +516,7 @@ def test_install_extra_path_dependency_does_not_exist(
508516
poetry = _project_factory(project, project_factory, fixture_dir)
509517
assert isinstance(poetry.locker, TestLocker)
510518
poetry.locker.locked(True)
511-
tester = command_tester_factory("install", poetry=poetry)
519+
tester = command_tester_factory(command, poetry=poetry)
512520
if not options:
513521
tester.execute(options)
514522
else:
@@ -519,6 +527,7 @@ def test_install_extra_path_dependency_does_not_exist(
519527
@pytest.mark.parametrize("options", ["", "--no-directory"])
520528
def test_install_missing_directory_dependency_with_no_directory(
521529
command_tester_factory: CommandTesterFactory,
530+
command: str,
522531
project_factory: ProjectFactory,
523532
fixture_dir: FixtureDirGetter,
524533
options: str,
@@ -528,7 +537,7 @@ def test_install_missing_directory_dependency_with_no_directory(
528537
)
529538
assert isinstance(poetry.locker, TestLocker)
530539
poetry.locker.locked(True)
531-
tester = command_tester_factory("install", poetry=poetry)
540+
tester = command_tester_factory(command, poetry=poetry)
532541
if options:
533542
tester.execute(options)
534543
else:
@@ -538,6 +547,7 @@ def test_install_missing_directory_dependency_with_no_directory(
538547

539548
def test_non_package_mode_does_not_try_to_install_root(
540549
command_tester_factory: CommandTesterFactory,
550+
command: str,
541551
project_factory: ProjectFactory,
542552
) -> None:
543553
content = """\
@@ -546,7 +556,7 @@ def test_non_package_mode_does_not_try_to_install_root(
546556
"""
547557
poetry = project_factory(name="non-package-mode", pyproject_content=content)
548558

549-
tester = command_tester_factory("install", poetry=poetry)
559+
tester = command_tester_factory(command, poetry=poetry)
550560
tester.execute()
551561

552562
assert tester.status_code == 0

tests/console/commands/test_sync.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
import pytest
6+
7+
from cleo.exceptions import CleoNoSuchOptionError
8+
9+
# import all tests from the install command
10+
# and run them for sync by overriding the command fixture
11+
from tests.console.commands.test_install import * # noqa: F403
12+
13+
14+
if TYPE_CHECKING:
15+
from cleo.testers.command_tester import CommandTester
16+
17+
18+
@pytest.fixture # type: ignore[no-redef]
19+
def command() -> str:
20+
return "sync"
21+
22+
23+
@pytest.mark.skip("Only relevant for `poetry install`") # type: ignore[no-redef]
24+
def test_sync_option_is_passed_to_the_installer() -> None:
25+
"""The only test from the install command that does not work for sync."""
26+
27+
28+
def test_sync_option_not_available(tester: CommandTester) -> None:
29+
with pytest.raises(CleoNoSuchOptionError):
30+
tester.execute("--sync")

0 commit comments

Comments
 (0)