This commit is contained in:
Andrey Rakhmatullin 2026-07-11 18:29:54 +00:00 committed by GitHub
commit 246f964e6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 56 additions and 87 deletions

View File

@ -3,6 +3,28 @@
Release notes Release notes
============= =============
Scrapy VERSION (unreleased)
---------------------------
Backward-incompatible changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- The following runtime usage of zope.interface_ interfaces is removed:
- :class:`~scrapy.spiderloader.SpiderLoader` and
:class:`~scrapy.spiderloader.DummySpiderLoader` are no longer marked
as implementing the ``ISpiderLoader`` interface.
- :func:`~scrapy.spiderloader.get_spider_loader` no longer checks that the
configured spider loader implements the ``ISpiderLoader`` interface.
- :class:`~scrapy.extensions.feedexport.BlockingFeedStorage`,
- :class:`~scrapy.extensions.feedexport.FileFeedStorage` and
- :class:`~scrapy.extensions.feedexport.StdoutFeedStorage` are no longer
marked as implementing the ``IFeedStorage`` interface.
(:issue:`6585`, :issue:`TBD`)
.. _release-2.17.0: .. _release-2.17.0:
Scrapy 2.17.0 (2026-07-07) Scrapy 2.17.0 (2026-07-07)
@ -2643,7 +2665,7 @@ Deprecation removals
(:issue:`6109`, :issue:`6116`) (:issue:`6109`, :issue:`6116`)
- A custom class assigned to the :setting:`SPIDER_LOADER_CLASS` setting that - A custom class assigned to the :setting:`SPIDER_LOADER_CLASS` setting that
does not implement the :class:`~scrapy.interfaces.ISpiderLoader` interface does not implement the ``ISpiderLoader`` interface
will now raise a :exc:`zope.interface.verify.DoesNotImplement` exception at will now raise a :exc:`zope.interface.verify.DoesNotImplement` exception at
run time. Non-compliant classes have been triggering a deprecation warning run time. Non-compliant classes have been triggering a deprecation warning
since Scrapy 1.0.0. since Scrapy 1.0.0.

View File

@ -178,9 +178,7 @@ SpiderLoader API
defined across the project. defined across the project.
Custom spider loaders can be employed by specifying their path in the Custom spider loaders can be employed by specifying their path in the
:setting:`SPIDER_LOADER_CLASS` project setting. They must fully implement :setting:`SPIDER_LOADER_CLASS` project setting.
the :class:`scrapy.interfaces.ISpiderLoader` interface to guarantee an
errorless execution.
.. method:: from_settings(settings) .. method:: from_settings(settings)

View File

@ -13,7 +13,6 @@ from twisted.internet.ssl import (
from twisted.web.client import BrowserLikePolicyForHTTPS from twisted.web.client import BrowserLikePolicyForHTTPS
from twisted.web.iweb import IPolicyForHTTPS from twisted.web.iweb import IPolicyForHTTPS
from zope.interface.declarations import implementer from zope.interface.declarations import implementer
from zope.interface.verify import verifyObject
from scrapy.core.downloader.tls import ( from scrapy.core.downloader.tls import (
_TWISTED_VERSION_MAP, _TWISTED_VERSION_MAP,
@ -232,7 +231,6 @@ class _AcceptableProtocolsContextFactory:
# all of this with _ScrapyClientContextFactory.acceptableProtocols. # all of this with _ScrapyClientContextFactory.acceptableProtocols.
def __init__(self, context_factory: Any, acceptable_protocols: list[bytes]): def __init__(self, context_factory: Any, acceptable_protocols: list[bytes]):
verifyObject(IPolicyForHTTPS, context_factory)
self._wrapped_context_factory: Any = context_factory self._wrapped_context_factory: Any = context_factory
self._acceptable_protocols: list[bytes] = acceptable_protocols self._acceptable_protocols: list[bytes] = acceptable_protocols

View File

@ -22,7 +22,7 @@ from urllib.parse import unquote, urlparse
from twisted.internet.defer import Deferred, DeferredList from twisted.internet.defer import Deferred, DeferredList
from w3lib.url import file_uri_to_path from w3lib.url import file_uri_to_path
from zope.interface import Interface, implementer from zope.interface import Interface
from scrapy import Spider, signals from scrapy import Spider, signals
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
@ -88,25 +88,18 @@ class ItemFilter:
return True # accept all items by default return True # accept all items by default
class IFeedStorage(Interface): # type: ignore[misc] class _IFeedStorage(Interface): # type: ignore[misc] # pragma: no cover
"""Interface that all Feed Storages must implement"""
# pylint: disable=no-self-argument # pylint: disable=no-self-argument
def __init__(uri, *, feed_options=None): # type: ignore[no-untyped-def] # pylint: disable=super-init-not-called def __init__(uri, *, feed_options=None): ... # type: ignore[no-untyped-def] # pylint: disable=super-init-not-called
"""Initialize the storage with the parameters given in the URI and the
feed-specific options (see :setting:`FEEDS`)"""
def open(spider): # type: ignore[no-untyped-def] def open(spider): ... # type: ignore[no-untyped-def]
"""Open the storage for the given spider. It must return a file-like
object that will be used for the exporters"""
def store(file): # type: ignore[no-untyped-def] def store(file): ... # type: ignore[no-untyped-def]
"""Store the given file stream"""
class FeedStorageProtocol(Protocol): class FeedStorageProtocol(Protocol):
"""Reimplementation of ``IFeedStorage`` that can be used in type hints.""" """Protocol that all Feed Storages must follow."""
def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None): def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None):
"""Initialize the storage with the parameters given in the URI and the """Initialize the storage with the parameters given in the URI and the
@ -120,7 +113,6 @@ class FeedStorageProtocol(Protocol):
"""Store the given file stream""" """Store the given file stream"""
@implementer(IFeedStorage)
class BlockingFeedStorage(ABC): class BlockingFeedStorage(ABC):
def open(self, spider: Spider) -> IO[bytes]: def open(self, spider: Spider) -> IO[bytes]:
path = spider.crawler.settings["FEED_TEMPDIR"] path = spider.crawler.settings["FEED_TEMPDIR"]
@ -137,7 +129,6 @@ class BlockingFeedStorage(ABC):
raise NotImplementedError raise NotImplementedError
@implementer(IFeedStorage)
class StdoutFeedStorage: class StdoutFeedStorage:
def __init__( def __init__(
self, self,
@ -164,7 +155,6 @@ class StdoutFeedStorage:
pass pass
@implementer(IFeedStorage)
class FileFeedStorage: class FileFeedStorage:
def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None): def __init__(self, uri: str, *, feed_options: dict[str, Any] | None = None):
self.path: str = file_uri_to_path(uri) if uri.startswith("file:") else uri self.path: str = file_uri_to_path(uri) if uri.startswith("file:") else uri
@ -731,3 +721,14 @@ class FeedExporter:
feed_options.get("item_filter", ItemFilter) feed_options.get("item_filter", ItemFilter)
) )
return item_filter_class(feed_options) return item_filter_class(feed_options)
def __getattr__(name: str) -> Any: # pragma: no cover
if name == "IFeedStorage":
warnings.warn(
"scrapy.extensions.feedexport.IFeedStorage is deprecated.",
ScrapyDeprecationWarning,
stacklevel=2,
)
return _IFeedStorage
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View File

@ -1,19 +1,23 @@
# pragma: no file cover
# pylint: disable=no-method-argument,no-self-argument # pylint: disable=no-method-argument,no-self-argument
import warnings
from zope.interface import Interface from zope.interface import Interface
from scrapy.exceptions import ScrapyDeprecationWarning
warnings.warn(
"The scrapy.interfaces module is deprecated.",
ScrapyDeprecationWarning,
stacklevel=2,
)
class ISpiderLoader(Interface): class ISpiderLoader(Interface):
def from_settings(settings): def from_settings(settings): ...
"""Return an instance of the class for the given settings"""
def load(spider_name): def load(spider_name): ...
"""Return the Spider class for the given spider name. If the spider
name is not found, it must raise a KeyError."""
def list(): def list(): ...
"""Return a list with the names of all spiders available in the
project"""
def find_by_request(request): def find_by_request(request): ...
"""Return the list of spiders names that can handle the given request"""

View File

@ -5,10 +5,6 @@ import warnings
from collections import defaultdict from collections import defaultdict
from typing import TYPE_CHECKING, Protocol, cast from typing import TYPE_CHECKING, Protocol, cast
from zope.interface import implementer
from zope.interface.verify import verifyClass
from scrapy.interfaces import ISpiderLoader
from scrapy.utils.misc import load_object, walk_modules_iter from scrapy.utils.misc import load_object, walk_modules_iter
from scrapy.utils.spider import iter_spider_classes from scrapy.utils.spider import iter_spider_classes
@ -26,7 +22,6 @@ def get_spider_loader(settings: BaseSettings) -> SpiderLoaderProtocol:
"""Get SpiderLoader instance from settings""" """Get SpiderLoader instance from settings"""
cls_path = settings.get("SPIDER_LOADER_CLASS") cls_path = settings.get("SPIDER_LOADER_CLASS")
loader_cls = load_object(cls_path) loader_cls = load_object(cls_path)
verifyClass(ISpiderLoader, loader_cls)
return cast("SpiderLoaderProtocol", loader_cls.from_settings(settings.frozencopy())) return cast("SpiderLoaderProtocol", loader_cls.from_settings(settings.frozencopy()))
@ -47,7 +42,6 @@ class SpiderLoaderProtocol(Protocol):
"""Return the list of spiders names that can handle the given request""" """Return the list of spiders names that can handle the given request"""
@implementer(ISpiderLoader)
class SpiderLoader: class SpiderLoader:
""" """
SpiderLoader is a class which locates and loads spiders SpiderLoader is a class which locates and loads spiders
@ -133,7 +127,6 @@ class SpiderLoader:
return list(self._spiders.keys()) return list(self._spiders.keys())
@implementer(ISpiderLoader)
class DummySpiderLoader: class DummySpiderLoader:
"""A dummy spider loader that does not load any spiders.""" """A dummy spider loader that does not load any spiders."""

View File

@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Any, ClassVar
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from zope.interface.exceptions import MultipleInvalid
import scrapy import scrapy
from scrapy import Spider from scrapy import Spider
@ -586,21 +585,7 @@ class TestCrawlerLogging:
assert "debug message" in logged assert "debug message" in logged
class SpiderLoaderWithWrongInterface:
def unneeded_method(self) -> None:
pass
class TestCrawlerRunner(TestBaseCrawler): class TestCrawlerRunner(TestBaseCrawler):
def test_spider_manager_verify_interface(self) -> None:
settings = Settings(
{
"SPIDER_LOADER_CLASS": SpiderLoaderWithWrongInterface,
}
)
with pytest.raises(MultipleInvalid):
CrawlerRunner(settings)
def test_crawler_runner_accepts_dict(self) -> None: def test_crawler_runner_accepts_dict(self) -> None:
runner = CrawlerRunner({"foo": "bar"}) runner = CrawlerRunner({"foo": "bar"})
assert runner.settings["foo"] == "bar" assert runner.settings["foo"] == "bar"
@ -612,15 +597,6 @@ class TestCrawlerRunner(TestBaseCrawler):
class TestAsyncCrawlerRunner(TestBaseCrawler): class TestAsyncCrawlerRunner(TestBaseCrawler):
def test_spider_manager_verify_interface(self) -> None:
settings = Settings(
{
"SPIDER_LOADER_CLASS": SpiderLoaderWithWrongInterface,
}
)
with pytest.raises(MultipleInvalid):
AsyncCrawlerRunner(settings)
def test_crawler_runner_accepts_dict(self) -> None: def test_crawler_runner_accepts_dict(self) -> None:
runner = AsyncCrawlerRunner({"foo": "bar"}) runner = AsyncCrawlerRunner({"foo": "bar"})
assert runner.settings["foo"] == "bar" assert runner.settings["foo"] == "bar"

View File

@ -20,7 +20,6 @@ import lxml.etree
import pytest import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from w3lib.url import file_uri_to_path from w3lib.url import file_uri_to_path
from zope.interface import implementer
import scrapy import scrapy
from scrapy import Spider, signals from scrapy import Spider, signals
@ -31,7 +30,6 @@ from scrapy.extensions.feedexport import (
FeedExporter, FeedExporter,
FeedSlot, FeedSlot,
FileFeedStorage, FileFeedStorage,
IFeedStorage,
) )
from scrapy.utils.python import to_unicode from scrapy.utils.python import to_unicode
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
@ -91,7 +89,6 @@ class FailingBlockingFeedStorage(DummyBlockingFeedStorage):
raise OSError("Cannot store") raise OSError("Cannot store")
@implementer(IFeedStorage)
class LogOnStoreFileStorage: class LogOnStoreFileStorage:
""" """
This storage logs inside `store` method. This storage logs inside `store` method.
@ -1234,7 +1231,6 @@ class TestFeedExport(TestFeedExportBase):
@coroutine_test @coroutine_test
async def test_storage_file_no_postprocessing(self): async def test_storage_file_no_postprocessing(self):
@implementer(IFeedStorage)
class Storage: class Storage:
def __init__(self, uri, *, feed_options=None): def __init__(self, uri, *, feed_options=None):
pass pass
@ -1256,7 +1252,6 @@ class TestFeedExport(TestFeedExportBase):
@coroutine_test @coroutine_test
async def test_storage_file_postprocessing(self): async def test_storage_file_postprocessing(self):
@implementer(IFeedStorage)
class Storage: class Storage:
def __init__(self, uri, *, feed_options=None): def __init__(self, uri, *, feed_options=None):
pass pass

View File

@ -12,12 +12,11 @@ from urllib.parse import urljoin
import lxml.etree import lxml.etree
import pytest import pytest
from packaging.version import Version from packaging.version import Version
from zope.interface.verify import verifyObject
import scrapy import scrapy
from scrapy import Spider from scrapy import Spider
from scrapy.exceptions import NotConfigured from scrapy.exceptions import NotConfigured
from scrapy.extensions.feedexport import FeedExporter, IFeedStorage, S3FeedStorage from scrapy.extensions.feedexport import FeedExporter, S3FeedStorage
from scrapy.settings import Settings from scrapy.settings import Settings
from scrapy.utils.python import to_unicode from scrapy.utils.python import to_unicode
from scrapy.utils.test import get_crawler from scrapy.utils.test import get_crawler
@ -435,9 +434,6 @@ class TestBatchDeliveries(TestFeedExportBase):
}, },
}, },
} }
crawler = get_crawler(settings_dict=settings)
storage = S3FeedStorage.from_crawler(crawler, uri)
verifyObject(IFeedStorage, storage)
class TestSpider(scrapy.Spider): class TestSpider(scrapy.Spider):
name = "testspider" name = "testspider"

View File

@ -12,7 +12,6 @@ from urllib.parse import quote
import pytest import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from w3lib.url import path_to_file_uri from w3lib.url import path_to_file_uri
from zope.interface.verify import verifyObject
import scrapy import scrapy
from scrapy.extensions.feedexport import ( from scrapy.extensions.feedexport import (
@ -20,7 +19,6 @@ from scrapy.extensions.feedexport import (
FileFeedStorage, FileFeedStorage,
FTPFeedStorage, FTPFeedStorage,
GCSFeedStorage, GCSFeedStorage,
IFeedStorage,
S3FeedStorage, S3FeedStorage,
StdoutFeedStorage, StdoutFeedStorage,
) )
@ -71,11 +69,6 @@ class TestFileFeedStorage:
finally: finally:
os.chdir(old_cwd) os.chdir(old_cwd)
def test_interface(self, tmp_path):
path = tmp_path / "file.txt"
st = FileFeedStorage(str(path))
verifyObject(IFeedStorage, st)
@staticmethod @staticmethod
def _store(path: Path, feed_options: dict[str, Any] | None = None) -> None: def _store(path: Path, feed_options: dict[str, Any] | None = None) -> None:
storage = FileFeedStorage(str(path), feed_options=feed_options) storage = FileFeedStorage(str(path), feed_options=feed_options)
@ -131,7 +124,6 @@ class TestFTPFeedStorage:
uri, uri,
feed_options=feed_options, feed_options=feed_options,
) )
verifyObject(IFeedStorage, storage)
spider = self.get_test_spider() spider = self.get_test_spider()
file = storage.open(spider) file = storage.open(spider)
file.write(content) file.write(content)
@ -275,7 +267,6 @@ class TestS3FeedStorage:
bucket = "mybucket" bucket = "mybucket"
key = "export.csv" key = "export.csv"
storage = S3FeedStorage.from_crawler(crawler, f"s3://{bucket}/{key}") storage = S3FeedStorage.from_crawler(crawler, f"s3://{bucket}/{key}")
verifyObject(IFeedStorage, storage)
file = mock.MagicMock() file = mock.MagicMock()

View File

@ -5,14 +5,12 @@ from pathlib import Path
from unittest import mock from unittest import mock
import pytest import pytest
from zope.interface.verify import verifyObject
# ugly hack to avoid cyclic imports of scrapy.spiders when running this test # ugly hack to avoid cyclic imports of scrapy.spiders when running this test
# alone # alone
import scrapy import scrapy
from scrapy.crawler import CrawlerRunner from scrapy.crawler import CrawlerRunner
from scrapy.http import Request from scrapy.http import Request
from scrapy.interfaces import ISpiderLoader
from scrapy.settings import Settings from scrapy.settings import Settings
from scrapy.spiderloader import DummySpiderLoader, SpiderLoader, get_spider_loader from scrapy.spiderloader import DummySpiderLoader, SpiderLoader, get_spider_loader
@ -45,9 +43,6 @@ def spider_loader(spider_loader_env):
class TestSpiderLoader: class TestSpiderLoader:
def test_interface(self, spider_loader):
verifyObject(ISpiderLoader, spider_loader)
def test_list(self, spider_loader): def test_list(self, spider_loader):
assert set(spider_loader.list()) == { assert set(spider_loader.list()) == {
"spider1", "spider1",