Skip to content
Merged
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
45 changes: 45 additions & 0 deletions src/poetry/console/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from typing import TYPE_CHECKING
from typing import cast

from cleo._utils import find_similar_names
from cleo.application import Application as BaseApplication
from cleo.events.console_command_event import ConsoleCommandEvent
from cleo.events.console_events import COMMAND
Expand Down Expand Up @@ -267,10 +268,54 @@ def _run(self, io: IO) -> int:
io.write_error_line(message)
return 1

if command is not None and command in self.get_namespaces():
sub_commands = []

for key in self._commands:
if key.startswith(f"{command} "):
sub_commands.append(key)

io.write_error_line(
f"The requested command does not exist in the <c1>{command}</> namespace."
)
suggested_names = find_similar_names(command, sub_commands)
self._error_write_command_suggestions(
io, suggested_names, f"#{command}"
)
return 1

if command is not None:
suggested_names = find_similar_names(
command, list(self._commands.keys())
)
io.write_error_line(
f"The requested command <c1>{command}</> does not exist."
)
self._error_write_command_suggestions(io, suggested_names)
return 1

raise e

return exit_code

def _error_write_command_suggestions(
self, io: IO, suggested_names: list[str], doc_tag: str | None = None
) -> None:
if suggested_names:
suggestion_lines = [
f"<c1>{name.replace(' ', '</> <b>', 1)}</>: {self._commands[name].description}"
for name in suggested_names
]
suggestions = "\n ".join(["", *sorted(suggestion_lines)])
io.write_error_line(
f"\n<error>Did you mean one of these perhaps?</>{suggestions}"
)

io.write_error_line(
"\n<b>Documentation: </>"
f"<info>https://python-poetry.org/docs/cli/{doc_tag or ''}</>"
)

def _configure_global_options(self, io: IO) -> None:
"""
Configures global options for the application by setting up the relevant
Expand Down
2 changes: 1 addition & 1 deletion tests/console/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def test_application_execute_plugin_command_with_plugins_disabled(
tester.execute("foo --no-plugins")

assert tester.io.fetch_output() == ""
assert tester.io.fetch_error() == '\nThe command "foo" does not exist.\n'
assert "The requested command foo does not exist." in tester.io.fetch_error()
assert tester.status_code == 1


Expand Down
63 changes: 63 additions & 0 deletions tests/console/test_application_command_not_found.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import pytest

from cleo.testers.application_tester import ApplicationTester

from poetry.console.application import Application


if TYPE_CHECKING:
from tests.types import CommandFactory


@pytest.fixture
def tester() -> ApplicationTester:
return ApplicationTester(Application())


@pytest.mark.parametrize(
("command", "suggested"),
[
("x", None),
("en", ["env activate", "env info", "env list", "env remove", "env use"]),
("sou", ["source add", "source remove", "source show"]),
],
)
def test_application_command_not_found_messages(
command: str,
suggested: list[str] | None,
tester: ApplicationTester,
command_factory: CommandFactory,
) -> None:
tester.execute(f"{command}")
assert tester.status_code != 0

stderr = tester.io.fetch_error()
assert f"The requested command {command} does not exist." in stderr

if suggested is None:
assert "Did you mean one of these perhaps?" not in stderr
else:
for suggestion in suggested:
assert suggestion in stderr


@pytest.mark.parametrize(
"namespace",
["cache", "debug", "env", "self", "source"],
)
def test_application_namespaced_command_not_found_messages(
namespace: str,
tester: ApplicationTester,
command_factory: CommandFactory,
) -> None:
tester.execute(f"{namespace} xxx")
assert tester.status_code != 0

stderr = tester.io.fetch_error()
assert (
f"The requested command does not exist in the {namespace} namespace." in stderr
)
2 changes: 1 addition & 1 deletion tests/console/test_application_removed_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_application_removed_command_default_message(

stderr = tester.io.fetch_error()
assert COMMAND_NOT_FOUND_PREFIX_MESSAGE not in stderr
assert 'The command "nonexistent" does not exist.' in stderr
assert "The requested command nonexistent does not exist." in stderr


@pytest.mark.parametrize(
Expand Down