mirror of https://github.com/scrapy/scrapy.git
Merge 7a80c63d8f into c9446931a8
This commit is contained in:
commit
0de9c7e2ce
|
|
@ -209,6 +209,48 @@ Project-only commands:
|
||||||
* :command:`edit`
|
* :command:`edit`
|
||||||
* :command:`parse`
|
* :command:`parse`
|
||||||
|
|
||||||
|
.. _topics-global-options:
|
||||||
|
|
||||||
|
Global options
|
||||||
|
==============
|
||||||
|
|
||||||
|
These options are available for all Scrapy commands:
|
||||||
|
|
||||||
|
``--logfile FILE``
|
||||||
|
Log output to *FILE* instead of stderr.
|
||||||
|
|
||||||
|
``-L LEVEL``, ``--loglevel LEVEL``
|
||||||
|
Set the logging level. Available levels (in increasing severity):
|
||||||
|
``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``, ``CRITICAL``.
|
||||||
|
Default: ``WARNING``.
|
||||||
|
|
||||||
|
``--nolog``
|
||||||
|
Disable logging completely.
|
||||||
|
|
||||||
|
``--pidfile FILE``
|
||||||
|
Write the process ID to *FILE*.
|
||||||
|
|
||||||
|
``-s NAME=VALUE``, ``--set NAME=VALUE``
|
||||||
|
Override a Scrapy setting (may be repeated)::
|
||||||
|
|
||||||
|
scrapy crawl myspider -s LOG_LEVEL=DEBUG -s CONCURRENT_REQUESTS=4
|
||||||
|
|
||||||
|
``--profile FILE``
|
||||||
|
Run the command under ``cProfile`` and write the binary stats dump to
|
||||||
|
*FILE*. After the run, a human-readable summary of the top 20 functions
|
||||||
|
sorted by cumulative time is printed to the log at ``INFO`` level.
|
||||||
|
|
||||||
|
Usage example::
|
||||||
|
|
||||||
|
scrapy crawl myspider --profile stats.prof
|
||||||
|
|
||||||
|
To inspect the saved binary dump later::
|
||||||
|
|
||||||
|
python -c "import pstats; pstats.Stats('stats.prof').sort_stats('cumulative').print_stats(20)"
|
||||||
|
|
||||||
|
``--pdb``
|
||||||
|
Enable ``pdb`` on failure.
|
||||||
|
|
||||||
.. command:: startproject
|
.. command:: startproject
|
||||||
|
|
||||||
startproject
|
startproject
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ from __future__ import annotations
|
||||||
import argparse
|
import argparse
|
||||||
import cProfile
|
import cProfile
|
||||||
import inspect
|
import inspect
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pstats
|
||||||
import sys
|
import sys
|
||||||
from importlib.metadata import entry_points
|
from importlib.metadata import entry_points
|
||||||
from typing import TYPE_CHECKING, ParamSpec
|
from typing import TYPE_CHECKING, ParamSpec
|
||||||
|
|
@ -24,6 +27,8 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
_P = ParamSpec("_P")
|
_P = ParamSpec("_P")
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ScrapyArgumentParser(argparse.ArgumentParser):
|
class ScrapyArgumentParser(argparse.ArgumentParser):
|
||||||
def _parse_optional(
|
def _parse_optional(
|
||||||
|
|
@ -232,6 +237,20 @@ def _run_command_profiled(
|
||||||
p.runctx("cmd.run(args, opts)", globals(), loc)
|
p.runctx("cmd.run(args, opts)", globals(), loc)
|
||||||
if opts.profile:
|
if opts.profile:
|
||||||
p.dump_stats(opts.profile)
|
p.dump_stats(opts.profile)
|
||||||
|
_log_profile_stats(p)
|
||||||
|
|
||||||
|
|
||||||
|
def _log_profile_stats(
|
||||||
|
p: cProfile.Profile,
|
||||||
|
sort: str = "cumulative",
|
||||||
|
lines: int = 20,
|
||||||
|
) -> None:
|
||||||
|
"""Log a human-readable cProfile stats summary at WARNING level."""
|
||||||
|
stream = io.StringIO()
|
||||||
|
ps = pstats.Stats(p, stream=stream)
|
||||||
|
ps.sort_stats(sort)
|
||||||
|
ps.print_stats(lines)
|
||||||
|
logger.warning("cProfile stats:\n%s", stream.getvalue())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -104,12 +104,6 @@ class ScrapyCommand(ABC):
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"--nolog", action="store_true", help="disable logging completely"
|
"--nolog", action="store_true", help="disable logging completely"
|
||||||
)
|
)
|
||||||
group.add_argument(
|
|
||||||
"--profile",
|
|
||||||
metavar="FILE",
|
|
||||||
default=None,
|
|
||||||
help="write python cProfile stats to FILE",
|
|
||||||
)
|
|
||||||
group.add_argument("--pidfile", metavar="FILE", help="write process ID to FILE")
|
group.add_argument("--pidfile", metavar="FILE", help="write process ID to FILE")
|
||||||
group.add_argument(
|
group.add_argument(
|
||||||
"-s",
|
"-s",
|
||||||
|
|
@ -119,6 +113,12 @@ class ScrapyCommand(ABC):
|
||||||
metavar="NAME=VALUE",
|
metavar="NAME=VALUE",
|
||||||
help="set/override setting (may be repeated)",
|
help="set/override setting (may be repeated)",
|
||||||
)
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--profile",
|
||||||
|
metavar="FILE",
|
||||||
|
default=None,
|
||||||
|
help="write python cProfile stats to FILE and print a stats summary",
|
||||||
|
)
|
||||||
group.add_argument("--pdb", action="store_true", help="enable pdb on failure")
|
group.add_argument("--pdb", action="store_true", help="enable pdb on failure")
|
||||||
|
|
||||||
def process_options(self, args: list[str], opts: argparse.Namespace) -> None:
|
def process_options(self, args: list[str], opts: argparse.Namespace) -> None:
|
||||||
|
|
|
||||||
|
|
@ -22,38 +22,44 @@ class TestCmdline:
|
||||||
encoding = sys.stdout.encoding or "utf-8"
|
encoding = sys.stdout.encoding or "utf-8"
|
||||||
args = (sys.executable, "-m", "scrapy.cmdline", *new_args)
|
args = (sys.executable, "-m", "scrapy.cmdline", *new_args)
|
||||||
proc = Popen(args, stdout=PIPE, stderr=PIPE, env=self.env, **kwargs)
|
proc = Popen(args, stdout=PIPE, stderr=PIPE, env=self.env, **kwargs)
|
||||||
comm = proc.communicate()[0].strip()
|
stdout, stderr = proc.communicate()
|
||||||
return comm.decode(encoding)
|
return stdout.strip().decode(encoding), stderr.strip().decode(encoding)
|
||||||
|
|
||||||
def test_default_settings(self):
|
def test_default_settings(self):
|
||||||
assert self._execute("settings", "--get", "TEST1") == "default"
|
stdout, _ = self._execute("settings", "--get", "TEST1")
|
||||||
|
assert stdout == "default"
|
||||||
|
|
||||||
def test_override_settings_using_set_arg(self):
|
def test_override_settings_using_set_arg(self):
|
||||||
assert (
|
stdout, _ = self._execute(
|
||||||
self._execute("settings", "--get", "TEST1", "-s", "TEST1=override")
|
"settings", "--get", "TEST1", "-s", "TEST1=override"
|
||||||
== "override"
|
|
||||||
)
|
)
|
||||||
|
assert stdout == "override"
|
||||||
|
|
||||||
def test_profiling(self):
|
def test_profiling(self):
|
||||||
path = Path(tempfile.mkdtemp())
|
path = Path(tempfile.mkdtemp())
|
||||||
filename = path / "res.prof"
|
filename = path / "res.prof"
|
||||||
try:
|
try:
|
||||||
self._execute("version", "--profile", str(filename))
|
_, log_output = self._execute("version", "--profile", str(filename))
|
||||||
|
# Binary dump file must be created
|
||||||
assert filename.exists()
|
assert filename.exists()
|
||||||
|
# Binary dump must contain valid cProfile data
|
||||||
out = StringIO()
|
out = StringIO()
|
||||||
stats = pstats.Stats(str(filename), stream=out)
|
stats = pstats.Stats(str(filename), stream=out)
|
||||||
stats.print_stats()
|
stats.print_stats()
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
stats = out.read()
|
stats_text = out.read()
|
||||||
assert str(Path("scrapy", "commands", "version.py")) in stats
|
assert str(Path("scrapy", "commands", "version.py")) in stats_text
|
||||||
assert "tottime" in stats
|
assert "tottime" in stats_text
|
||||||
|
# Human-readable summary must appear in the log (stderr)
|
||||||
|
assert "cProfile stats" in log_output
|
||||||
|
assert "cumtime" in log_output
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
|
|
||||||
def test_override_dict_settings(self):
|
def test_override_dict_settings(self):
|
||||||
EXT_PATH = "tests.test_cmdline.extensions.DummyExtension"
|
EXT_PATH = "tests.test_cmdline.extensions.DummyExtension"
|
||||||
EXTENSIONS = {EXT_PATH: 200}
|
EXTENSIONS = {EXT_PATH: 200}
|
||||||
settingsstr = self._execute(
|
stdout, _ = self._execute(
|
||||||
"settings",
|
"settings",
|
||||||
"--get",
|
"--get",
|
||||||
"EXTENSIONS",
|
"EXTENSIONS",
|
||||||
|
|
@ -61,14 +67,16 @@ class TestCmdline:
|
||||||
"EXTENSIONS=" + json.dumps(EXTENSIONS),
|
"EXTENSIONS=" + json.dumps(EXTENSIONS),
|
||||||
)
|
)
|
||||||
# XXX: There's gotta be a smarter way to do this...
|
# XXX: There's gotta be a smarter way to do this...
|
||||||
assert "..." not in settingsstr
|
assert "..." not in stdout
|
||||||
for char in ("'", "<", ">"):
|
for char in ("'", "<", ">"):
|
||||||
settingsstr = settingsstr.replace(char, '"')
|
stdout = stdout.replace(char, '"')
|
||||||
settingsdict = json.loads(settingsstr)
|
settingsdict = json.loads(stdout)
|
||||||
assert set(settingsdict.keys()) == set(EXTENSIONS.keys())
|
assert set(settingsdict.keys()) == set(EXTENSIONS.keys())
|
||||||
assert settingsdict[EXT_PATH] == 200
|
assert settingsdict[EXT_PATH] == 200
|
||||||
|
|
||||||
def test_pathlib_path_as_feeds_key(self):
|
def test_pathlib_path_as_feeds_key(self):
|
||||||
assert self._execute("settings", "--get", "FEEDS") == json.dumps(
|
stdout, _ = self._execute("settings", "--get", "FEEDS")
|
||||||
|
assert stdout == json.dumps(
|
||||||
{"items.csv": {"format": "csv", "fields": ["price", "name"]}}
|
{"items.csv": {"format": "csv", "fields": ["price", "name"]}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue