mirror of https://github.com/scrapy/scrapy.git
Improve test coverage for scrapy.cmdline (#7795)
This commit is contained in:
parent
5a65bdcc18
commit
ad816d2b3a
|
|
@ -225,13 +225,11 @@ def _run_command(cmd: ScrapyCommand, args: list[str], opts: argparse.Namespace)
|
|||
def _run_command_profiled(
|
||||
cmd: ScrapyCommand, args: list[str], opts: argparse.Namespace
|
||||
) -> None:
|
||||
if opts.profile:
|
||||
sys.stderr.write(f"scrapy: writing cProfile stats to {opts.profile!r}\n")
|
||||
sys.stderr.write(f"scrapy: writing cProfile stats to {opts.profile!r}\n")
|
||||
loc = locals()
|
||||
p = cProfile.Profile()
|
||||
p.runctx("cmd.run(args, opts)", globals(), loc)
|
||||
if opts.profile:
|
||||
p.dump_stats(opts.profile)
|
||||
p.dump_stats(opts.profile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -3,14 +3,12 @@ from __future__ import annotations
|
|||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from io import StringIO
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
import scrapy
|
||||
from scrapy.cmdline import _pop_command_name, _print_unknown_command_msg
|
||||
from scrapy.cmdline import _pop_command_name, execute
|
||||
from scrapy.commands import ScrapyCommand, ScrapyHelpFormatter, view
|
||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||
from scrapy.settings import Settings
|
||||
|
|
@ -153,12 +151,6 @@ class MySpider(scrapy.Spider):
|
|||
|
||||
self._append_settings(proj_mod_path, "LOG_LEVEL = 'DEBUG'\n")
|
||||
|
||||
@staticmethod
|
||||
def _append_settings(proj_mod_path: Path, text: str) -> None:
|
||||
"""Add text to the end of the project settings.py."""
|
||||
with (proj_mod_path / "settings.py").open("a", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
@staticmethod
|
||||
def _replace_custom_settings(
|
||||
proj_mod_path: Path, spider_name: str, text: str
|
||||
|
|
@ -347,23 +339,223 @@ class TestMiscCommands(TestProjectBase):
|
|||
subdir.mkdir(exist_ok=True)
|
||||
assert call("list", cwd=subdir) == 0
|
||||
|
||||
def test_command_not_found(self) -> None:
|
||||
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", False, na_msg),
|
||||
("abc", False, not_found_msg),
|
||||
("abc", True, 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)
|
||||
assert out.getvalue().strip() == message.strip()
|
||||
|
||||
class TestCommandListing(TestProjectBase):
|
||||
"""Tests for the command list that ``scrapy`` prints when called without a
|
||||
command name."""
|
||||
|
||||
def test_outside_project(self) -> None:
|
||||
returncode, out, err = proc()
|
||||
assert returncode == 0, err
|
||||
assert f"Scrapy {scrapy.__version__} - no active project" in out
|
||||
assert "Available commands:" in out
|
||||
assert "Create new project" in out
|
||||
assert "More commands available when run from project directory" in out
|
||||
assert 'Use "scrapy <command> -h" to see more info about a command' in out
|
||||
|
||||
def test_inside_project(self, proj_path: Path) -> None:
|
||||
returncode, out, err = proc(cwd=proj_path)
|
||||
assert returncode == 0, err
|
||||
assert (
|
||||
f"Scrapy {scrapy.__version__} - active project: {self.project_name}" in out
|
||||
)
|
||||
assert "List available spiders" in out
|
||||
assert "More commands available when run from project directory" not in out
|
||||
|
||||
|
||||
class TestUnknownCommand(TestProjectBase):
|
||||
def test_outside_project(self) -> None:
|
||||
returncode, out, err = proc("abc")
|
||||
assert returncode == 2, err
|
||||
assert f"Scrapy {scrapy.__version__} - no active project" in out
|
||||
assert "Unknown command: abc" in out
|
||||
assert 'Use "scrapy" to see available commands' in out
|
||||
|
||||
def test_inside_project(self, proj_path: Path) -> None:
|
||||
returncode, out, err = proc("abc", cwd=proj_path)
|
||||
assert returncode == 2, err
|
||||
assert (
|
||||
f"Scrapy {scrapy.__version__} - active project: {self.project_name}" in out
|
||||
)
|
||||
assert "Unknown command: abc" in out
|
||||
|
||||
def test_project_only_command_outside_project(self) -> None:
|
||||
returncode, out, err = proc("list")
|
||||
assert returncode == 2, err
|
||||
assert "The list command is not available from this location." in out
|
||||
assert (
|
||||
"These commands are only available from within a project: "
|
||||
"check, crawl, edit, list, parse." in out
|
||||
)
|
||||
|
||||
|
||||
class TestCommandsModule(TestProjectBase):
|
||||
"""Tests for commands defined in the module of the COMMANDS_MODULE setting."""
|
||||
|
||||
@pytest.fixture
|
||||
def proj_path_with_commands(self, proj_path: Path) -> Path:
|
||||
commands_path = proj_path / self.project_name / "commands"
|
||||
commands_path.mkdir()
|
||||
(commands_path / "__init__.py").touch()
|
||||
(commands_path / "mycmd.py").write_text(
|
||||
"""
|
||||
from scrapy.commands import ScrapyCommand
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_crawler_process = False
|
||||
|
||||
def short_desc(self):
|
||||
return "My custom command"
|
||||
|
||||
def run(self, args, opts):
|
||||
print("My custom command ran")
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(commands_path / "helpcmd.py").write_text(
|
||||
"""
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.exceptions import UsageError
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_crawler_process = False
|
||||
|
||||
def short_desc(self):
|
||||
return "My command that asks for its help message"
|
||||
|
||||
def run(self, args, opts):
|
||||
raise UsageError
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(commands_path / "silentcmd.py").write_text(
|
||||
"""
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.exceptions import UsageError
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_crawler_process = False
|
||||
|
||||
def short_desc(self):
|
||||
return "My command that fails silently"
|
||||
|
||||
def run(self, args, opts):
|
||||
raise UsageError(print_help=False)
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
self._append_settings(
|
||||
proj_path / self.project_name,
|
||||
f'\nCOMMANDS_MODULE = "{self.project_name}.commands"\n',
|
||||
)
|
||||
return proj_path
|
||||
|
||||
def test_listed(self, proj_path_with_commands: Path) -> None:
|
||||
returncode, out, err = proc(cwd=proj_path_with_commands)
|
||||
assert returncode == 0, err
|
||||
assert "My custom command" in out
|
||||
|
||||
def test_run(self, proj_path_with_commands: Path) -> None:
|
||||
returncode, out, err = proc("mycmd", cwd=proj_path_with_commands)
|
||||
assert returncode == 0, err
|
||||
assert "My custom command ran" in out
|
||||
|
||||
def test_usage_error(self, proj_path_with_commands: Path) -> None:
|
||||
"""A message-less UsageError makes the help message be printed."""
|
||||
returncode, out, err = proc("helpcmd", cwd=proj_path_with_commands)
|
||||
assert returncode == 2, err
|
||||
assert "scrapy helpcmd" in out
|
||||
|
||||
def test_usage_error_without_help(self, proj_path_with_commands: Path) -> None:
|
||||
"""A message-less UsageError with print_help disabled prints nothing."""
|
||||
returncode, out, err = proc("silentcmd", cwd=proj_path_with_commands)
|
||||
assert returncode == 2, err
|
||||
assert not out
|
||||
|
||||
|
||||
class TestEntryPointCommands:
|
||||
"""Tests for commands defined in the scrapy.commands entry point group."""
|
||||
|
||||
@staticmethod
|
||||
def _write_dist(path: Path, entry_point: str) -> None:
|
||||
"""Write into *path* a package with a command and a function, and the
|
||||
metadata of an installed distribution that declares *entry_point* in
|
||||
the scrapy.commands entry point group.
|
||||
|
||||
Since ``python -m scrapy.cmdline`` puts the current working directory
|
||||
in the import path, running it with *path* as the working directory
|
||||
makes Scrapy find that entry point.
|
||||
"""
|
||||
package_path = path / "mycmds"
|
||||
package_path.mkdir()
|
||||
(package_path / "__init__.py").touch()
|
||||
(package_path / "mycmd.py").write_text(
|
||||
"""
|
||||
from scrapy.commands import ScrapyCommand
|
||||
|
||||
|
||||
class Command(ScrapyCommand):
|
||||
requires_crawler_process = False
|
||||
|
||||
def short_desc(self):
|
||||
return "My entry point command"
|
||||
|
||||
def run(self, args, opts):
|
||||
print("My entry point command ran")
|
||||
|
||||
|
||||
def not_a_command():
|
||||
pass
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
dist_info_path = path / "mycmds-1.0.dist-info"
|
||||
dist_info_path.mkdir()
|
||||
(dist_info_path / "METADATA").write_text(
|
||||
"Metadata-Version: 2.1\nName: mycmds\nVersion: 1.0\n", encoding="utf-8"
|
||||
)
|
||||
(dist_info_path / "entry_points.txt").write_text(
|
||||
f"[scrapy.commands]\n{entry_point}\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
def test_listed(self, tmp_path: Path) -> None:
|
||||
self._write_dist(tmp_path, "mycmd = mycmds.mycmd:Command")
|
||||
returncode, out, err = proc(cwd=tmp_path)
|
||||
assert returncode == 0, err
|
||||
assert "My entry point command" in out
|
||||
|
||||
def test_run(self, tmp_path: Path) -> None:
|
||||
self._write_dist(tmp_path, "mycmd = mycmds.mycmd:Command")
|
||||
returncode, out, err = proc("mycmd", cwd=tmp_path)
|
||||
assert returncode == 0, err
|
||||
assert "My entry point command ran" in out
|
||||
|
||||
def test_not_a_class(self, tmp_path: Path) -> None:
|
||||
self._write_dist(tmp_path, "mycmd = mycmds.mycmd:not_a_command")
|
||||
returncode, _, err = proc("version", cwd=tmp_path)
|
||||
assert returncode == 1
|
||||
assert "ValueError: Invalid entry point mycmd" in err
|
||||
|
||||
|
||||
class TestExecute:
|
||||
"""Tests for calls to scrapy.cmdline.execute() from Python code, which the
|
||||
command line does not cover."""
|
||||
|
||||
def test_argv(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
execute(["scrapy", "version"])
|
||||
assert exc_info.value.code == 0
|
||||
assert scrapy.__version__ in capsys.readouterr().out
|
||||
|
||||
def test_settings(self, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
settings = Settings()
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
execute(["scrapy", "settings", "--get", "BOT_NAME"], settings=settings)
|
||||
assert exc_info.value.code == 0
|
||||
assert capsys.readouterr().out.strip() == "scrapybot"
|
||||
|
||||
|
||||
class TestBenchCommand:
|
||||
|
|
|
|||
|
|
@ -32,3 +32,9 @@ class TestProjectBase:
|
|||
proj_path = tmp_path / self.project_name
|
||||
copytree(_proj_path_cached, proj_path)
|
||||
return proj_path
|
||||
|
||||
@staticmethod
|
||||
def _append_settings(proj_mod_path: Path, text: str) -> None:
|
||||
"""Add text to the end of the project settings.py."""
|
||||
with (proj_mod_path / "settings.py").open("a", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
|
|
|||
Loading…
Reference in New Issue