From 8a1b38d852c7aadc7f94555beb115eed2902fe49 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Mon, 16 Feb 2026 11:10:59 -0800 Subject: [PATCH] Validate package names in poetry update command Detect version specifiers in package arguments and raise a clear error pointing users to `poetry add` instead. Also validate that all specified packages are declared dependencies of the project. Fixes #10422 --- src/poetry/console/commands/update.py | 38 +++++++++++++++ tests/console/commands/test_update.py | 69 +++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/src/poetry/console/commands/update.py b/src/poetry/console/commands/update.py index b749b01ab6c..d1dec7e80fc 100644 --- a/src/poetry/console/commands/update.py +++ b/src/poetry/console/commands/update.py @@ -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 @@ -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" @@ -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( + "Version specifiers are not allowed in" + " poetry update." + ) + for pkg in packages_with_specifiers: + self.line_error(f" - {pkg}") + self.line_error( + "Use poetry add 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( + "The following packages are not dependencies" + f" of this project: {', '.join(invalid_packages)}" + ) + return 1 + self.installer.whitelist(dict.fromkeys(packages, "*")) self.installer.only_groups(self.activated_groups) diff --git a/tests/console/commands/test_update.py b/tests/console/commands/test_update.py index 6b48071d612..3f8da1d87ee 100644 --- a/tests/console/commands/test_update.py +++ b/tests/console/commands/test_update.py @@ -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