diff --git a/scrapy/cmdline.py b/scrapy/cmdline.py index 48f462c65..065adccfb 100644 --- a/scrapy/cmdline.py +++ b/scrapy/cmdline.py @@ -89,6 +89,12 @@ def _get_commands_dict( return cmds +def _get_project_only_cmds(settings: BaseSettings) -> set[str]: + return set(_get_commands_dict(settings, inproject=True)) - set( + _get_commands_dict(settings, inproject=False) + ) + + def _pop_command_name(argv: list[str]) -> str | None: for i, arg in enumerate(argv[1:]): if not arg.startswith("-"): @@ -121,11 +127,25 @@ def _print_commands(settings: BaseSettings, inproject: bool) -> None: print('Use "scrapy -h" to see more info about a command') +def _print_unknown_command_msg( + settings: BaseSettings, cmdname: str, inproject: bool +) -> None: + proj_only_cmds = _get_project_only_cmds(settings) + if cmdname in proj_only_cmds and not inproject: + cmd_list = ", ".join(sorted(proj_only_cmds)) + print( + f"The {cmdname} command is not available from this location.\n" + f"These commands are only available from within a project: {cmd_list}.\n" + ) + else: + print(f"Unknown command: {cmdname}\n") + + def _print_unknown_command( settings: BaseSettings, cmdname: str, inproject: bool ) -> None: _print_header(settings, inproject) - print(f"Unknown command: {cmdname}\n") + _print_unknown_command_msg(settings, cmdname, inproject) print('Use "scrapy" to see available commands') diff --git a/tests/test_commands.py b/tests/test_commands.py index 9d5720b98..1aae3222e 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -9,6 +9,7 @@ import re import subprocess import sys from contextlib import contextmanager +from io import StringIO from itertools import chain from pathlib import Path from shutil import copytree, rmtree @@ -16,12 +17,13 @@ from stat import S_IWRITE as ANYONE_WRITE_PERMISSION from tempfile import TemporaryFile, mkdtemp from threading import Timer from typing import TYPE_CHECKING -from unittest import skipIf +from unittest import mock, skipIf from pytest import mark from twisted.trial import unittest import scrapy +from scrapy.cmdline import _print_unknown_command_msg from scrapy.commands import ScrapyCommand, ScrapyHelpFormatter, view from scrapy.commands.startproject import IGNORE from scrapy.settings import Settings @@ -652,6 +654,24 @@ class MiscCommandsTest(CommandTest): def test_list(self): self.assertEqual(0, self.call("list")) + def test_command_not_found(self): + na_msg = """ +The list command is not available from this location. +These commands are only available from within a project: check, crawl, edit, list, parse. +""" + not_found_msg = """ +Unknown command: abc +""" + params = [ + ("list", 0, na_msg), + ("abc", 0, not_found_msg), + ("abc", 1, not_found_msg), + ] + for cmdname, inproject, message in params: + with mock.patch("sys.stdout", new=StringIO()) as out: + _print_unknown_command_msg(Settings(), cmdname, inproject) + self.assertEqual(out.getvalue().strip(), message.strip()) + class RunSpiderCommandTest(CommandTest): spider_filename = "myspider.py"