mirror of https://github.com/scrapy/scrapy.git
254 lines
8.5 KiB
Python
254 lines
8.5 KiB
Python
from __future__ import annotations
|
|
|
|
import numbers
|
|
import os
|
|
import sys
|
|
import warnings
|
|
from configparser import ConfigParser
|
|
from operator import itemgetter
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
|
|
from scrapy.exceptions import UsageError
|
|
from scrapy.settings import BaseSettings
|
|
from scrapy.utils.deprecate import update_classpath
|
|
from scrapy.utils.python import without_none_values
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable, Collection, Iterable, Mapping, MutableMapping
|
|
|
|
if sys.version_info >= (3, 11):
|
|
import tomllib
|
|
else:
|
|
import tomli as tomllib
|
|
|
|
|
|
def build_component_list(
|
|
compdict: MutableMapping[Any, Any],
|
|
*,
|
|
convert: Callable[[Any], Any] = update_classpath,
|
|
) -> list[Any]:
|
|
"""Compose a component list from a :ref:`component priority dictionary
|
|
<component-priority-dictionaries>`."""
|
|
|
|
def _check_components(complist: Collection[Any]) -> None:
|
|
if len({convert(c) for c in complist}) != len(complist):
|
|
raise ValueError(
|
|
f"Some paths in {complist!r} convert to the same object, "
|
|
"please update your settings"
|
|
)
|
|
|
|
def _map_keys(compdict: Mapping[Any, Any]) -> BaseSettings | dict[Any, Any]:
|
|
if isinstance(compdict, BaseSettings):
|
|
compbs = BaseSettings()
|
|
for k, v in compdict.items():
|
|
prio = compdict.getpriority(k)
|
|
assert prio is not None
|
|
if compbs.getpriority(convert(k)) == prio:
|
|
raise ValueError(
|
|
f"Some paths in {list(compdict.keys())!r} "
|
|
"convert to the same "
|
|
"object, please update your settings"
|
|
)
|
|
compbs.set(convert(k), v, priority=prio)
|
|
return compbs
|
|
_check_components(compdict)
|
|
return {convert(k): v for k, v in compdict.items()}
|
|
|
|
def _validate_values(compdict: Mapping[Any, Any]) -> None:
|
|
"""Fail if a value in the components dict is not a real number or None."""
|
|
for name, value in compdict.items():
|
|
if value is not None and not isinstance(value, numbers.Real):
|
|
raise ValueError(
|
|
f"Invalid value {value} for component {name}, "
|
|
"please provide a real number or None instead"
|
|
)
|
|
|
|
_validate_values(compdict)
|
|
compdict = without_none_values(_map_keys(compdict))
|
|
return [k for k, v in sorted(compdict.items(), key=itemgetter(1))]
|
|
|
|
|
|
def arglist_to_dict(arglist: list[str]) -> dict[str, str]:
|
|
"""Convert a list of arguments like ['arg1=val1', 'arg2=val2', ...] to a
|
|
dict
|
|
"""
|
|
return dict(x.split("=", 1) for x in arglist)
|
|
|
|
|
|
def closest_config(
|
|
path: str | os.PathLike = ".", _resolved: Path | None = None
|
|
) -> tuple[str, str]:
|
|
"""
|
|
Returns the closest config file (pyproject.toml or scrapy.cfg) by
|
|
traversing the current directory and its parents
|
|
"""
|
|
current = Path(path).resolve() if _resolved is None else _resolved
|
|
|
|
toml = current / "pyproject.toml"
|
|
if toml.exists():
|
|
try:
|
|
with toml.open("rb") as f:
|
|
data = tomllib.load(f)
|
|
if data.get("tool", {}).get("scrapy"):
|
|
return "toml", str(toml)
|
|
except tomllib.TOMLDecodeError:
|
|
warnings.warn(
|
|
f"Scrapy could not parse {toml}: invalid TOML. "
|
|
"This file will be ignored.",
|
|
stacklevel=2,
|
|
)
|
|
|
|
cfgfile = current / "scrapy.cfg"
|
|
if cfgfile.exists():
|
|
return "cfg", str(cfgfile)
|
|
|
|
parent = current.parent
|
|
if parent == current:
|
|
return "", ""
|
|
|
|
return closest_config(_resolved=parent)
|
|
|
|
|
|
def closest_scrapy_cfg(
|
|
path: str | os.PathLike = ".",
|
|
prevpath: str | os.PathLike | None = None,
|
|
) -> str:
|
|
"""Return the path to the closest scrapy.cfg file by traversing the current
|
|
directory and its parents
|
|
"""
|
|
if prevpath is not None and str(path) == str(prevpath):
|
|
return ""
|
|
path = Path(path).resolve()
|
|
cfgfile = path / "scrapy.cfg"
|
|
if cfgfile.exists():
|
|
return str(cfgfile)
|
|
return closest_scrapy_cfg(path.parent, path)
|
|
|
|
|
|
def init_env(project: str = "default", set_syspath: bool = True) -> None:
|
|
"""Initialize environment to use command-line tool from inside a project
|
|
dir. This sets the Scrapy settings module and modifies the Python path to
|
|
be able to locate the project module.
|
|
"""
|
|
cfg = get_config()
|
|
if cfg.has_option("settings", project):
|
|
os.environ["SCRAPY_SETTINGS_MODULE"] = cfg.get("settings", project)
|
|
|
|
_, closest = closest_config()
|
|
if closest:
|
|
projdir = str(Path(closest).parent)
|
|
if set_syspath and projdir not in sys.path:
|
|
sys.path.append(projdir)
|
|
|
|
|
|
def get_config(use_closest: bool = True) -> ConfigParser:
|
|
"""Get Scrapy config file as a ConfigParser"""
|
|
if use_closest:
|
|
config_type, config_path = closest_config()
|
|
if config_type == "toml":
|
|
with Path(config_path).open("rb") as f:
|
|
data = tomllib.load(f)
|
|
scrapy_data = data.get("tool", {}).get("scrapy", {})
|
|
cfg = ConfigParser()
|
|
cfg.read_dict(scrapy_data)
|
|
return cfg
|
|
if config_type == "cfg":
|
|
warnings.warn(
|
|
"scrapy.cfg is deprecated. Please use pyproject.toml instead.",
|
|
DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
cfg = ConfigParser()
|
|
cfg.read(config_path)
|
|
return cfg
|
|
|
|
sources = get_sources()
|
|
cfg = ConfigParser()
|
|
cfg.read(sources)
|
|
return cfg
|
|
|
|
|
|
def get_sources() -> list[str]:
|
|
xdg_config_home = (
|
|
os.environ.get("XDG_CONFIG_HOME") or Path("~/.config").expanduser()
|
|
)
|
|
return [
|
|
"/etc/scrapy.cfg",
|
|
r"c:\scrapy\scrapy.cfg",
|
|
str(Path(xdg_config_home) / "scrapy.cfg"),
|
|
str(Path("~/.scrapy.cfg").expanduser()),
|
|
]
|
|
|
|
|
|
def feed_complete_default_values_from_settings(
|
|
feed: dict[str, Any], settings: BaseSettings
|
|
) -> dict[str, Any]:
|
|
out = feed.copy()
|
|
out.setdefault("batch_item_count", settings.getint("FEED_EXPORT_BATCH_ITEM_COUNT"))
|
|
out.setdefault("encoding", settings["FEED_EXPORT_ENCODING"])
|
|
out.setdefault("fields", settings.getdictorlist("FEED_EXPORT_FIELDS") or None)
|
|
out.setdefault("store_empty", settings.getbool("FEED_STORE_EMPTY"))
|
|
out.setdefault("uri_params", settings["FEED_URI_PARAMS"])
|
|
out.setdefault("item_export_kwargs", {})
|
|
if settings["FEED_EXPORT_INDENT"] is None:
|
|
out.setdefault("indent", None)
|
|
else:
|
|
out.setdefault("indent", settings.getint("FEED_EXPORT_INDENT"))
|
|
return out
|
|
|
|
|
|
def feed_process_params_from_cli(
|
|
settings: BaseSettings,
|
|
output: list[str],
|
|
*,
|
|
overwrite_output: list[str] | None = None,
|
|
) -> dict[str, dict[str, Any]]:
|
|
"""
|
|
Receives feed export params (from the 'crawl' or 'runspider' commands),
|
|
checks for inconsistencies in their quantities and returns a dictionary
|
|
suitable to be used as the FEEDS setting.
|
|
"""
|
|
valid_output_formats: Iterable[str] = without_none_values(
|
|
cast("dict[str, str]", settings.getwithbase("FEED_EXPORTERS"))
|
|
).keys()
|
|
|
|
def check_valid_format(output_format: str) -> None:
|
|
if output_format not in valid_output_formats:
|
|
raise UsageError(
|
|
f"Unrecognized output format '{output_format}'. "
|
|
f"Set a supported one ({tuple(valid_output_formats)}) "
|
|
"after a colon at the end of the output URI (i.e. -o/-O "
|
|
"<URI>:<FORMAT>) or as a file extension."
|
|
)
|
|
|
|
overwrite = False
|
|
if overwrite_output:
|
|
if output:
|
|
raise UsageError(
|
|
"Please use only one of -o/--output and -O/--overwrite-output"
|
|
)
|
|
output = overwrite_output
|
|
overwrite = True
|
|
|
|
result: dict[str, dict[str, Any]] = {}
|
|
for element in output:
|
|
try:
|
|
feed_uri, feed_format = element.rsplit(":", 1)
|
|
check_valid_format(feed_format)
|
|
except (ValueError, UsageError):
|
|
feed_uri = element
|
|
feed_format = Path(element).suffix.replace(".", "")
|
|
else:
|
|
if feed_uri == "-":
|
|
feed_uri = "stdout:"
|
|
check_valid_format(feed_format)
|
|
result[feed_uri] = {"format": feed_format}
|
|
if overwrite:
|
|
result[feed_uri]["overwrite"] = True
|
|
|
|
# FEEDS setting should take precedence over the matching CLI options
|
|
result.update(settings.getdict("FEEDS"))
|
|
|
|
return result
|