Switch Scrapy projects to pyproject.toml and ~/.scrapy/config.toml

This commit is contained in:
Adrian Chaves 2026-07-30 14:38:41 +02:00
parent 98696efa80
commit af7d9f41b1
16 changed files with 432 additions and 60 deletions

View File

@ -56,7 +56,7 @@ directory where you'd like to store your code and run::
This will create a ``tutorial`` directory with the following contents::
tutorial/
scrapy.cfg # deploy configuration file
pyproject.toml # project configuration file
tutorial/ # project's Python module, you'll import your code from here
__init__.py

View File

@ -25,6 +25,19 @@ Backward-incompatible changes
(:issue:`6585`, :issue:`7731`)
- New projects created with the :command:`startproject` command define their
configuration in a :file:`pyproject.toml` file, instead of the now
deprecated :file:`scrapy.cfg` file.
To deploy such a project you need ``scrapyd`` TODO or higher,
``scrapyd-client`` TODO or higher, or ``shub`` TODO or higher. Earlier
versions of those tools only read :file:`scrapy.cfg` files.
(:issue:`7030`)
.. TODO: Fill in the ``scrapyd``, ``scrapyd-client`` and ``shub`` versions
above. Those releases, with support for the ``[tool.scrapy]`` table of
:file:`pyproject.toml`, must happen before this Scrapy release.
.. _release-2.17.0:
Scrapy 2.17.0 (2026-07-07)

View File

@ -16,22 +16,19 @@ accepts a different set of arguments and options.
(The ``scrapy deploy`` command has been removed in 1.0 in favor of the
standalone ``scrapyd-deploy``. See `Deploying your project`_.)
.. _config:
.. _topics-config-settings:
Configuration settings
======================
Scrapy will look for configuration parameters in ini-style ``scrapy.cfg`` files
in standard locations:
Scrapy reads configuration parameters from the ``[tool.scrapy]`` table of the
:file:`pyproject.toml` file at the root of your project (see next section):
1. ``/etc/scrapy.cfg`` or ``c:\scrapy\scrapy.cfg`` (system-wide),
2. ``~/.config/scrapy.cfg`` (``$XDG_CONFIG_HOME``) and ``~/.scrapy.cfg`` (``$HOME``)
for global (user-wide) settings, and
3. ``scrapy.cfg`` inside a Scrapy project's root (see next section).
.. code-block:: toml
Settings from these files are merged in the listed order of preference:
user-defined values have higher priority than system-wide defaults
and project-wide settings will override all others, when defined.
[tool.scrapy.settings]
default = "myproject.settings"
Scrapy also understands, and can be configured through, a number of environment
variables. Currently these are:
@ -40,6 +37,28 @@ variables. Currently these are:
* ``SCRAPY_PROJECT`` (see :ref:`topics-project-envvar`)
* ``SCRAPY_PYTHON_SHELL`` (see :ref:`topics-shell`)
.. _global-config:
Global configuration
--------------------
Project-independent configuration parameters may also be set for all your
projects in the :file:`.scrapy/config.toml` file of your home folder, i.e.
:file:`~/.scrapy/config.toml` on Linux and macOS or
:file:`%USERPROFILE%\\.scrapy\\config.toml` on Windows:
.. code-block:: toml
[settings]
shell = "bpython"
Only the following parameters are supported there:
* ``settings.shell`` (see :ref:`topics-shell`)
Any project may override them, using the same section and parameter name in its
:file:`pyproject.toml` file.
.. _topics-project-structure:
Default structure of Scrapy projects
@ -51,7 +70,7 @@ understand the directory structure of a Scrapy project.
Though it can be modified, all Scrapy projects have the same file
structure by default, similar to this::
scrapy.cfg
pyproject.toml
myproject/
__init__.py
items.py
@ -64,32 +83,32 @@ structure by default, similar to this::
spider2.py
...
The directory where the ``scrapy.cfg`` file resides is known as the *project
root directory*. That file contains the name of the python module that defines
the project settings. Here is an example:
The directory where the :file:`pyproject.toml` file resides is known as the
*project root directory*. That file contains the name of the python module that
defines the project settings. Here is an example:
.. code-block:: ini
.. code-block:: toml
[settings]
default = myproject.settings
[tool.scrapy.settings]
default = "myproject.settings"
.. _topics-project-envvar:
Sharing the root directory between projects
===========================================
A project root directory, the one that contains the ``scrapy.cfg``, may be
shared by multiple Scrapy projects, each with its own settings module.
A project root directory, the one that contains the :file:`pyproject.toml`, may
be shared by multiple Scrapy projects, each with its own settings module.
In that case, you must define one or more aliases for those settings modules
under ``[settings]`` in your ``scrapy.cfg`` file:
under ``[tool.scrapy.settings]`` in your :file:`pyproject.toml` file:
.. code-block:: ini
.. code-block:: toml
[settings]
default = myproject1.settings
project1 = myproject1.settings
project2 = myproject2.settings
[tool.scrapy.settings]
default = "myproject1.settings"
project1 = "myproject1.settings"
project2 = "myproject2.settings"
By default, the ``scrapy`` command-line tool will use the ``default`` settings.
Use the ``SCRAPY_PROJECT`` environment variable to specify a different project

View File

@ -46,7 +46,7 @@ tool.
Please refer to the `Zyte Scrapy Cloud documentation`_ for more information.
Zyte Scrapy Cloud is compatible with Scrapyd and one can switch between
them as needed - the configuration is read from the ``scrapy.cfg`` file
them as needed - the configuration is read from the :file:`pyproject.toml` file
just like ``scrapyd-deploy``.
.. _Deploying your project: https://scrapyd.readthedocs.io/en/latest/deploy.html

View File

@ -37,12 +37,21 @@ Through Scrapy's settings you can configure it to use any one of
``ptpython``, ``ipython``, ``bpython`` or the standard ``python`` shell,
regardless of which are installed. This is done by setting the
``SCRAPY_PYTHON_SHELL`` environment variable; or by defining it in your
:ref:`scrapy.cfg <topics-config-settings>`:
:file:`pyproject.toml` file (see :ref:`config`):
.. code-block:: ini
.. code-block:: toml
[tool.scrapy.settings]
shell = "bpython"
You can also define it for all your projects in the
:file:`.scrapy/config.toml` file of your home folder (see
:ref:`global-config`):
.. code-block:: toml
[settings]
shell = bpython
shell = "bpython"
.. _ptpython: https://github.com/prompt-toolkit/ptpython
.. _IPython: https://ipython.org/

View File

@ -21,6 +21,7 @@ dependencies = [
"queuelib>=1.4.2",
"service_identity>=23.1.0",
"tldextract",
"tomli>=2.0.1; python_version < '3.11'",
"w3lib>=1.17.0",
"zope.interface>=5.1.0",
# Platform-specific dependencies

View File

@ -11,13 +11,14 @@ from typing import TYPE_CHECKING, Any, ClassVar
import scrapy
from scrapy.commands import ScrapyCommand
from scrapy.exceptions import UsageError
from scrapy.utils.conf import _scrapy_table
from scrapy.utils.template import render_templatefile, string_camelcase
if TYPE_CHECKING:
import argparse
TEMPLATES_TO_RENDER: tuple[tuple[str, ...], ...] = (
("scrapy.cfg",),
("pyproject.toml",),
("${project_name}", "settings.py.tmpl"),
("${project_name}", "items.py.tmpl"),
("${project_name}", "pipelines.py.tmpl"),
@ -102,6 +103,21 @@ class Command(ScrapyCommand):
print(f"Error: scrapy.cfg already exists in {project_dir.resolve()}")
return
pyproject_path = project_dir / "pyproject.toml"
# Read before the template overwrites it, to restore it below.
pyproject_content = (
pyproject_path.read_text(encoding="utf-8")
if pyproject_path.is_file()
else None
)
if pyproject_content is not None and _scrapy_table(pyproject_path) is not None:
self.exitcode = 1
print(
"Error: pyproject.toml already has a [tool.scrapy] table in "
f"{project_dir.resolve()}"
)
return
if not self._is_valid_name(project_name):
self.exitcode = 1
return
@ -121,6 +137,15 @@ class Command(ScrapyCommand):
project_name=project_name,
ProjectName=string_camelcase(project_name),
)
if pyproject_content:
# Append the generated tables to the pre-existing pyproject.toml
# instead of replacing it.
pyproject_path.write_text(
pyproject_content.rstrip("\n")
+ "\n\n"
+ pyproject_path.read_text(encoding="utf-8"),
encoding="utf-8",
)
print(
f"New Scrapy project '{project_name}', using template directory "
f"'{self.templates_dir}', created in:\n",

View File

@ -162,13 +162,12 @@ class Shell:
if self.code:
print(eval(self.code, globals(), self.vars)) # noqa: S307
else:
# Detect interactive shell setting in scrapy.cfg
# e.g.: ~/.config/scrapy.cfg or ~/.scrapy.cfg
# [settings]
# Detect interactive shell setting in pyproject.toml
# [tool.scrapy.settings]
# # shell can be one of ipython, bpython or python;
# # to be used as the interactive python console, if available.
# # (default is ipython, fallbacks in the order listed above)
# shell = python
# shell = "python"
cfg = get_config()
section, option = "settings", "shell"
env = os.environ.get("SCRAPY_PYTHON_SHELL")

View File

@ -0,0 +1,2 @@
[tool.scrapy.settings]
default = "${project_name}.settings"

View File

@ -1,11 +0,0 @@
# Automatically created by: scrapy startproject
#
# For more information about the [deploy] section see:
# https://scrapyd.readthedocs.io/en/latest/deploy.html
[settings]
default = ${project_name}.settings
[deploy]
#url = http://localhost:6800/
project = ${project_name}

View File

@ -3,12 +3,13 @@ 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.exceptions import ScrapyDeprecationWarning, UsageError
from scrapy.settings import BaseSettings
from scrapy.utils.deprecate import update_classpath
from scrapy.utils.python import without_none_values
@ -16,6 +17,17 @@ 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
_GLOBAL_CONFIG_PATH = Path("~/.scrapy/config.toml")
# Project-independent options that the global configuration file supports, as
# (section, option) pairs.
_GLOBAL_OPTIONS = frozenset({("settings", "shell")})
def build_component_list(
compdict: MutableMapping[Any, Any],
@ -70,20 +82,105 @@ def arglist_to_dict(arglist: list[str]) -> dict[str, str]:
return dict(x.split("=", 1) for x in arglist)
def _load_toml(path: Path) -> dict[str, Any] | None:
"""Return the content of the specified TOML file, or ``None`` if it cannot
be parsed.
"""
try:
with path.open("rb") as f:
return tomllib.load(f)
except tomllib.TOMLDecodeError as exc:
warnings.warn(f"Ignoring invalid TOML file {path}: {exc}", stacklevel=3)
return None
def _scrapy_table(path: Path) -> dict[str, Any] | None:
"""Return the ``tool.scrapy`` table of the specified TOML file, or ``None``
if the file has no such table or cannot be parsed.
"""
data = _load_toml(path)
tool = data.get("tool") if data else None
table = tool.get("scrapy") if isinstance(tool, dict) else None
return table if isinstance(table, dict) else None
def _read_global_config(cfg: ConfigParser) -> None:
"""Read the supported options of the global configuration file into the
specified :class:`~configparser.ConfigParser` object.
"""
path = _GLOBAL_CONFIG_PATH.expanduser()
if not path.is_file():
return
ignored: list[str] = []
for section, options in (_load_toml(path) or {}).items():
if not isinstance(options, dict):
ignored.append(section)
continue
for option, value in options.items():
if (section, option) not in _GLOBAL_OPTIONS:
ignored.append(f"{section}.{option}")
continue
if not cfg.has_section(section):
cfg.add_section(section)
cfg.set(section, option, str(value))
if ignored:
supported = ", ".join(f"{s}.{o}" for s, o in sorted(_GLOBAL_OPTIONS))
warnings.warn(
f"Ignoring the following options of {path}: {', '.join(ignored)}. "
f"Only project-independent options are supported there: {supported}.",
stacklevel=3,
)
def closest_config(path: str | os.PathLike[str] = ".") -> str:
"""Return the path to the closest configuration file, found by traversing
the specified folder and its parents.
A folder holds a configuration file if it contains a
:file:`pyproject.toml` file with a ``tool.scrapy`` table or a (deprecated)
:file:`scrapy.cfg` file. If it contains both, :file:`pyproject.toml` wins.
"""
start = Path(path).resolve()
for folder in (start, *start.parents):
toml_path = folder / "pyproject.toml"
if toml_path.is_file() and _scrapy_table(toml_path) is not None:
return str(toml_path)
cfg_path = folder / "scrapy.cfg"
if cfg_path.is_file():
return str(cfg_path)
return ""
def closest_scrapy_cfg(
path: str | os.PathLike[str] = ".",
prevpath: str | os.PathLike[str] | None = None,
) -> str:
"""Return the path to the closest scrapy.cfg file by traversing the current
directory and its parents
.. deprecated:: VERSION
Use :func:`closest_config` instead.
"""
warnings.warn(
"scrapy.utils.conf.closest_scrapy_cfg() is deprecated, use "
"scrapy.utils.conf.closest_config() instead.",
ScrapyDeprecationWarning,
stacklevel=2,
)
return _closest_scrapy_cfg(path, prevpath)
def _closest_scrapy_cfg(
path: str | os.PathLike[str] = ".",
prevpath: str | os.PathLike[str] | None = None,
) -> str:
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)
return _closest_scrapy_cfg(path.parent, path)
def init_env(project: str = "default", set_syspath: bool = True) -> None:
@ -94,7 +191,7 @@ def init_env(project: str = "default", set_syspath: bool = True) -> None:
cfg = get_config()
if cfg.has_option("settings", project):
os.environ["SCRAPY_SETTINGS_MODULE"] = cfg.get("settings", project)
closest = closest_scrapy_cfg()
closest = closest_config()
if closest:
projdir = str(Path(closest).parent)
if set_syspath and projdir not in sys.path:
@ -103,9 +200,32 @@ def init_env(project: str = "default", set_syspath: bool = True) -> None:
def get_config(use_closest: bool = True) -> ConfigParser:
"""Get Scrapy config file as a ConfigParser"""
sources = get_sources(use_closest)
cfg = ConfigParser()
cfg.read(sources)
global_files = cfg.read(get_sources(use_closest=False))
if global_files:
warnings.warn(
f"Configuration read from {', '.join(global_files)}. Global "
"scrapy.cfg files are deprecated, use "
f"{_GLOBAL_CONFIG_PATH.expanduser()} for "
"project-independent options and the [tool.scrapy] table of the "
"pyproject.toml file of your project for the rest. See "
"https://docs.scrapy.org/en/latest/topics/commands.html#global-config",
ScrapyDeprecationWarning,
stacklevel=2,
)
_read_global_config(cfg)
closest = closest_config() if use_closest else ""
if Path(closest).name == "pyproject.toml":
cfg.read_dict(_scrapy_table(Path(closest)) or {})
elif closest:
warnings.warn(
f"{closest} is deprecated, define your project configuration in "
"the [tool.scrapy] table of a pyproject.toml file instead. See "
"https://docs.scrapy.org/en/latest/topics/commands.html#config",
ScrapyDeprecationWarning,
stacklevel=2,
)
cfg.read(closest)
return cfg
@ -120,7 +240,7 @@ def get_sources(use_closest: bool = True) -> list[str]:
str(Path("~/.scrapy.cfg").expanduser()),
]
if use_closest:
sources.append(closest_scrapy_cfg())
sources.append(_closest_scrapy_cfg())
return sources

View File

@ -7,7 +7,7 @@ from pathlib import Path
from scrapy.exceptions import NotConfigured
from scrapy.settings import Settings
from scrapy.utils.conf import closest_scrapy_cfg, get_config, init_env
from scrapy.utils.conf import closest_config, get_config, init_env
ENVVAR = "SCRAPY_SETTINGS_MODULE"
DATADIR_CFG_SECTION = "datadir"
@ -25,7 +25,7 @@ def inside_project() -> bool:
)
else:
return True
return bool(closest_scrapy_cfg())
return bool(closest_config())
def project_data_dir(project: str = "default") -> str:
@ -36,12 +36,12 @@ def project_data_dir(project: str = "default") -> str:
if cfg.has_option(DATADIR_CFG_SECTION, project):
d = Path(cfg.get(DATADIR_CFG_SECTION, project))
else:
scrapy_cfg = closest_scrapy_cfg()
if not scrapy_cfg:
config = closest_config()
if not config:
raise NotConfigured(
"Unable to find scrapy.cfg file to infer project data dir"
"Unable to find a pyproject.toml file to infer project data dir"
)
d = (Path(scrapy_cfg).parent / ".scrapy").resolve()
d = (Path(config).parent / ".scrapy").resolve()
if not d.exists():
d.mkdir(parents=True)
return str(d)

View File

@ -20,7 +20,7 @@ class TestStartprojectCommand:
@staticmethod
def _assert_files_exist(project_dir: Path, project_name: str) -> None:
assert (project_dir / "scrapy.cfg").exists()
assert (project_dir / "pyproject.toml").exists()
assert (project_dir / project_name).exists()
assert (project_dir / project_name / "__init__.py").exists()
assert (project_dir / project_name / "items.py").exists()
@ -77,6 +77,34 @@ class TestStartprojectCommand:
assert call("startproject", project_name, cwd=tmp_path) == 0
self._assert_files_exist(project_path, project_name)
def test_existing_pyproject_toml(self, tmp_path: Path) -> None:
pyproject_path = tmp_path / "pyproject.toml"
pyproject_path.write_text('[project]\nname = "existing"\n', encoding="utf-8")
assert call("startproject", self.project_name, ".", cwd=tmp_path) == 0
self._assert_files_exist(tmp_path, self.project_name)
content = pyproject_path.read_text(encoding="utf-8")
assert '[project]\nname = "existing"\n' in content
assert f'default = "{self.project_name}.settings"' in content
def test_existing_scrapy_table(self, tmp_path: Path) -> None:
(tmp_path / "pyproject.toml").write_text(
'[tool.scrapy.deploy]\nproject = "existing"\n', encoding="utf-8"
)
returncode, out, _ = proc("startproject", self.project_name, ".", cwd=tmp_path)
assert returncode == 1
assert "already has a [tool.scrapy] table" in out
def test_existing_scrapy_cfg(self, tmp_path: Path) -> None:
(tmp_path / "scrapy.cfg").write_text(
"[deploy]\nproject = existing\n", encoding="utf-8"
)
returncode, out, _ = proc("startproject", self.project_name, ".", cwd=tmp_path)
assert returncode == 1
assert "scrapy.cfg already exists" in out
def get_permissions_dict(
path: str | os.PathLike[str], renamings=None, ignore=None

View File

@ -1,16 +1,21 @@
from __future__ import annotations
import warnings
from pathlib import Path
from typing import Any
import pytest
from scrapy.exceptions import UsageError
from scrapy.exceptions import ScrapyDeprecationWarning, UsageError
from scrapy.settings import BaseSettings, Settings
from scrapy.utils.conf import (
arglist_to_dict,
build_component_list,
closest_config,
closest_scrapy_cfg,
feed_complete_default_values_from_settings,
feed_process_params_from_cli,
get_config,
)
@ -58,6 +63,166 @@ def test_arglist_to_dict():
}
SETTINGS_TOML = (
"[tool.scrapy.settings]\n"
'default = "myproject1.settings"\n'
'project1 = "myproject1.settings"\n'
'project2 = "myproject2.settings"\n'
)
SETTINGS_CFG = (
"[settings]\ndefault = myproject.settings\n[deploy]\nproject = myproject\n"
)
class TestConfig:
@pytest.fixture(autouse=True)
def home(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Point the global config locations (see
:func:`scrapy.utils.conf.get_sources`) at an initially empty folder."""
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HOME", str(home))
monkeypatch.setenv("USERPROFILE", str(home))
monkeypatch.setenv("XDG_CONFIG_HOME", str(home))
return home
def test_no_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.chdir(tmp_path)
assert closest_config() == ""
def test_pyproject_toml(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
(tmp_path / "pyproject.toml").write_text(SETTINGS_TOML, encoding="utf-8")
subdir = tmp_path / "a" / "b"
subdir.mkdir(parents=True)
monkeypatch.chdir(subdir)
assert Path(closest_config()) == (tmp_path / "pyproject.toml").resolve()
with warnings.catch_warnings():
warnings.simplefilter("error", ScrapyDeprecationWarning)
cfg = get_config()
assert cfg.get("settings", "default") == "myproject1.settings"
assert cfg.get("settings", "project1") == "myproject1.settings"
assert cfg.get("settings", "project2") == "myproject2.settings"
def test_pyproject_toml_preferred(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
(tmp_path / "pyproject.toml").write_text(SETTINGS_TOML, encoding="utf-8")
(tmp_path / "scrapy.cfg").write_text(SETTINGS_CFG, encoding="utf-8")
monkeypatch.chdir(tmp_path)
assert Path(closest_config()) == (tmp_path / "pyproject.toml").resolve()
cfg = get_config()
assert cfg.get("settings", "default") == "myproject1.settings"
assert not cfg.has_section("deploy")
def test_closest_wins(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
(tmp_path / "pyproject.toml").write_text(SETTINGS_TOML, encoding="utf-8")
subdir = tmp_path / "a"
subdir.mkdir()
(subdir / "scrapy.cfg").write_text(SETTINGS_CFG, encoding="utf-8")
monkeypatch.chdir(subdir)
assert Path(closest_config()) == (subdir / "scrapy.cfg").resolve()
with pytest.warns(ScrapyDeprecationWarning, match="scrapy.cfg is deprecated"):
cfg = get_config()
assert cfg.get("settings", "default") == "myproject.settings"
def test_pyproject_toml_without_scrapy_table(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
(tmp_path / "pyproject.toml").write_text(SETTINGS_TOML, encoding="utf-8")
subdir = tmp_path / "a"
subdir.mkdir()
(subdir / "pyproject.toml").write_text(
'[project]\nname = "unrelated"\n', encoding="utf-8"
)
monkeypatch.chdir(subdir)
assert Path(closest_config()) == (tmp_path / "pyproject.toml").resolve()
def test_invalid_pyproject_toml(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
(tmp_path / "pyproject.toml").write_text(
"[tool.scrapy\nnot valid toml", encoding="utf-8"
)
(tmp_path / "scrapy.cfg").write_text(SETTINGS_CFG, encoding="utf-8")
monkeypatch.chdir(tmp_path)
with pytest.warns(UserWarning, match="Ignoring invalid TOML file"):
closest = closest_config()
assert Path(closest) == (tmp_path / "scrapy.cfg").resolve()
@staticmethod
def _write_global_config(home: Path, content: str) -> None:
path = home / ".scrapy" / "config.toml"
path.parent.mkdir()
path.write_text(content, encoding="utf-8")
def test_global_config(
self, home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
self._write_global_config(home, '[settings]\nshell = "bpython"\n')
monkeypatch.chdir(tmp_path)
assert get_config().get("settings", "shell") == "bpython"
def test_global_config_overridden_by_project(
self, home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
self._write_global_config(home, '[settings]\nshell = "bpython"\n')
(tmp_path / "pyproject.toml").write_text(
'[tool.scrapy.settings]\nshell = "python"\n', encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
assert get_config().get("settings", "shell") == "python"
def test_global_config_unsupported_options(
self, home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
self._write_global_config(
home,
'[settings]\nshell = "bpython"\ndefault = "myproject.settings"\n'
'[deploy]\nproject = "myproject"\nunsupported = 1\n',
)
monkeypatch.chdir(tmp_path)
with pytest.warns(UserWarning, match="settings.default, deploy.project"):
cfg = get_config()
assert cfg.get("settings", "shell") == "bpython"
assert not cfg.has_option("settings", "default")
assert not cfg.has_section("deploy")
def test_global_scrapy_cfg_deprecated(
self, home: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
(home / "scrapy.cfg").write_text(
"[settings]\nshell = python\n", encoding="utf-8"
)
monkeypatch.chdir(tmp_path)
with pytest.warns(
ScrapyDeprecationWarning, match="Global scrapy.cfg files are deprecated"
):
cfg = get_config()
assert cfg.get("settings", "shell") == "python"
def test_closest_scrapy_cfg_deprecated(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
(tmp_path / "scrapy.cfg").write_text(SETTINGS_CFG, encoding="utf-8")
monkeypatch.chdir(tmp_path)
with pytest.warns(ScrapyDeprecationWarning, match="closest_scrapy_cfg"):
assert Path(closest_scrapy_cfg()) == (tmp_path / "scrapy.cfg").resolve()
class TestFeedExportConfig:
def test_feed_export_config_invalid_format(self):
settings = Settings()

View File

@ -20,7 +20,7 @@ def proj_path(tmp_path: Path) -> Generator[Path]:
try:
os.chdir(project_dir)
Path("scrapy.cfg").touch()
Path("pyproject.toml").write_text("[tool.scrapy]\n", encoding="utf-8")
yield project_dir
finally:

View File

@ -82,6 +82,7 @@ deps =
pyOpenSSL==26.3.0
pytest==9.1.1
socksio==1.0.0
tomli==2.4.1
types-Pygments==2.20.0.20260518
types-defusedxml==0.7.0.20260504
types-lxml==2026.2.16
@ -142,6 +143,7 @@ deps =
pyOpenSSL==22.0.0
queuelib==1.4.2
service_identity==23.1.0
tomli==2.0.1
w3lib==1.17.0
zope.interface==5.1.0
{[test-requirements]deps}