Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/poetry/console/commands/update.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import annotations

import re

from typing import TYPE_CHECKING
from typing import ClassVar

from cleo.helpers import argument
from cleo.helpers import option
from packaging.utils import canonicalize_name

from poetry.console.commands.installer_command import InstallerCommand

Expand All @@ -13,6 +16,8 @@
from cleo.io.inputs.argument import Argument
from cleo.io.inputs.option import Option

_VERSION_SPECIFIER_RE = re.compile(r"[><=!~]")


class UpdateCommand(InstallerCommand):
name = "update"
Expand Down Expand Up @@ -45,6 +50,39 @@ class UpdateCommand(InstallerCommand):
def handle(self) -> int:
packages = self.argument("packages")
if packages:
# Detect version specifiers in package arguments — poetry update
# only accepts bare package names, not requirement strings.
packages_with_specifiers = [
p for p in packages if _VERSION_SPECIFIER_RE.search(p)
]
if packages_with_specifiers:
self.line_error(
"<error>Version specifiers are not allowed in"
" <c1>poetry update</c1>.</error>"
)
for pkg in packages_with_specifiers:
self.line_error(f" - {pkg}")
self.line_error(
"Use <c1>poetry add</c1> to change version constraints."
)
return 1

# Validate that all specified packages are declared dependencies
all_dependencies = {
canonicalize_name(dep.name) for dep in self.poetry.package.all_requires
}

invalid_packages = [
p for p in packages if canonicalize_name(p) not in all_dependencies
]

if invalid_packages:
self.line_error(
"<error>The following packages are not dependencies"
f" of this project: {', '.join(invalid_packages)}</error>"
)
return 1

self.installer.whitelist(dict.fromkeys(packages, "*"))

self.installer.only_groups(self.activated_groups)
Expand Down
69 changes: 69 additions & 0 deletions tests/console/commands/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,72 @@ def test_update_sync_option_is_passed_to_the_installer(
tester.execute("--sync")

assert tester.command.installer._requires_synchronization


def test_update_with_invalid_package_name_shows_error(
poetry_with_outdated_lockfile: Poetry,
command_tester_factory: CommandTesterFactory,
) -> None:
"""
Providing non-existent package names should raise an error.
"""
tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile)

status = tester.execute("nonexistent-package")

assert status == 1
assert (
"The following packages are not dependencies of this project: nonexistent-package"
in tester.io.fetch_error()
)


def test_update_with_multiple_invalid_package_names_shows_error(
poetry_with_outdated_lockfile: Poetry,
command_tester_factory: CommandTesterFactory,
) -> None:
"""
Providing multiple non-existent package names should list all of them in the error.
"""
tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile)

status = tester.execute("fake1 fake2 fake3")

assert status == 1
error = tester.io.fetch_error()
assert "The following packages are not dependencies of this project" in error
assert "fake1" in error
assert "fake2" in error
assert "fake3" in error


@pytest.mark.parametrize(
"package_spec",
[
"docker==1.2.3",
"docker>=1.0,<2.0",
"docker!=1.0",
"docker[extra]>=1.0",
"nonexistent==1.2.3",
"nonexistent>=1.0",
],
)
def test_update_with_version_specifier_raises_error(
package_spec: str,
poetry_with_outdated_lockfile: Poetry,
command_tester_factory: CommandTesterFactory,
) -> None:
"""
The update command only accepts bare package names. Passing requirement
strings with version specifiers should raise a clear error pointing
to poetry add, regardless of whether the package is a dependency.
"""
tester = command_tester_factory("update", poetry=poetry_with_outdated_lockfile)

status = tester.execute(package_spec)

assert status == 1
error = tester.io.fetch_error()
assert "Version specifiers are not allowed" in error
assert "poetry update" in error
assert "poetry add" in error