mirror of https://github.com/scrapy/scrapy.git
Remove more code.
This commit is contained in:
parent
760c0db094
commit
9612ae3e93
|
|
@ -17,40 +17,26 @@ Activating and configuring add-ons
|
||||||
Add-ons and their configuration live in Scrapy's
|
Add-ons and their configuration live in Scrapy's
|
||||||
:class:`~scrapy.addons.AddonManager`. During a :class:`~scrapy.crawler.Crawler`
|
:class:`~scrapy.addons.AddonManager`. During a :class:`~scrapy.crawler.Crawler`
|
||||||
initialization the add-on manager will read a list of enabled add-ons from your
|
initialization the add-on manager will read a list of enabled add-ons from your
|
||||||
``ADDONS`` setting and their optional configuration from the respective
|
``ADDONS`` setting.
|
||||||
settings.
|
|
||||||
|
|
||||||
The ``ADDONS`` setting is a dict in which every key is an addon class or its
|
The ``ADDONS`` setting is a dict in which every key is an addon class or its
|
||||||
import path and the value is its priority.
|
import path and the value is its priority.
|
||||||
|
|
||||||
The configuration of an add-on, if necessary at all, is stored as a dictionary
|
This is an example where two add-ons are enabled in a project's
|
||||||
setting whose name is the uppercase add-on name.
|
``settings.py``::
|
||||||
|
|
||||||
This is an example where two add-ons (in this case with one requiring no
|
|
||||||
configuration) are enabled/configured in a project's ``settings.py``::
|
|
||||||
|
|
||||||
ADDONS = {
|
ADDONS = {
|
||||||
'path.to.someaddon': 0,
|
'path.to.someaddon': 0,
|
||||||
path.to.someaddon2: 1,
|
path.to.someaddon2: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
SOMEADDON = {
|
|
||||||
'some_config': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Writing your own add-ons
|
Writing your own add-ons
|
||||||
========================
|
========================
|
||||||
|
|
||||||
Add-ons are (any) Python *objects* that provide Scrapy's *add-on interface*:
|
Add-ons are (any) Python *objects* that include the following method:
|
||||||
|
|
||||||
.. attribute:: name
|
.. method:: update_settings(settings)
|
||||||
|
|
||||||
string with add-on name
|
|
||||||
|
|
||||||
:type: ``str``
|
|
||||||
|
|
||||||
.. method:: update_settings(config, settings)
|
|
||||||
|
|
||||||
This method is called during the initialization of the
|
This method is called during the initialization of the
|
||||||
:class:`~scrapy.crawler.Crawler`. Here, you should perform dependency checks
|
:class:`~scrapy.crawler.Crawler`. Here, you should perform dependency checks
|
||||||
|
|
@ -58,88 +44,26 @@ Add-ons are (any) Python *objects* that provide Scrapy's *add-on interface*:
|
||||||
:class:`~scrapy.settings.Settings` object as wished, e.g. enable components
|
:class:`~scrapy.settings.Settings` object as wished, e.g. enable components
|
||||||
for this add-on or set required configuration of other extensions.
|
for this add-on or set required configuration of other extensions.
|
||||||
|
|
||||||
:param config: Configuration of this add-on
|
|
||||||
:type config: ``dict``
|
|
||||||
|
|
||||||
:param settings: The settings object storing Scrapy/component configuration
|
:param settings: The settings object storing Scrapy/component configuration
|
||||||
:type settings: :class:`~scrapy.settings.Settings`
|
:type settings: :class:`~scrapy.settings.Settings`
|
||||||
|
|
||||||
.. method:: check_configuration(config, crawler)
|
|
||||||
|
|
||||||
This method is called when the :class:`~scrapy.crawler.Crawler` has been
|
|
||||||
fully initialized, immediately before it starts crawling. You can perform
|
|
||||||
additional dependency and configuration checks here.
|
|
||||||
|
|
||||||
:param config: Configuration of this add-on
|
|
||||||
:type config: ``dict``
|
|
||||||
|
|
||||||
:param crawler: Fully initialized Scrapy crawler
|
|
||||||
:type crawler: :class:`~scrapy.crawler.Crawler`
|
|
||||||
|
|
||||||
|
|
||||||
Add-on base class
|
|
||||||
=================
|
|
||||||
|
|
||||||
Scrapy comes with a built-in base class for add-ons which provides some
|
|
||||||
convenience functionality: the add-on configuration can be exposed into
|
|
||||||
Scrapy's settings via :meth:`~scrapy.addons.Addon.export_config`, configurable
|
|
||||||
via :attr:`~scrapy.addons.Addon.default_config` and
|
|
||||||
:attr:`~scrapy.addons.Addon.config_mapping`.
|
|
||||||
|
|
||||||
By default, the base add-on class will expose the add-on configuration into
|
|
||||||
Scrapy's settings namespace, in upper case. It is
|
|
||||||
easy to write your own functionality while still being able to use the
|
|
||||||
convenience functions by overwriting
|
|
||||||
:meth:`~scrapy.addons.Addon.update_settings`.
|
|
||||||
|
|
||||||
.. module:: scrapy.addons
|
|
||||||
:noindex:
|
|
||||||
|
|
||||||
.. autoclass:: Addon
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
Add-on examples
|
Add-on examples
|
||||||
===============
|
===============
|
||||||
|
|
||||||
Set some basic configuration using the :class:`Addon` base class::
|
Set some basic configuration::
|
||||||
|
|
||||||
from scrapy.addons import Addon
|
class MyAddon:
|
||||||
|
def update_settings(self, settings):
|
||||||
class MyAddon(Addon):
|
|
||||||
name = 'myaddon'
|
|
||||||
|
|
||||||
def update_settings(self, config, settings):
|
|
||||||
super().update_settings(settings)
|
|
||||||
settings["ITEM_PIPELINES"]["path.to.mypipeline"] = 200
|
settings["ITEM_PIPELINES"]["path.to.mypipeline"] = 200
|
||||||
settings["DNSCACHE_ENABLED"] = True
|
settings["DNSCACHE_ENABLED"] = True
|
||||||
|
|
||||||
Check dependencies::
|
Check dependencies::
|
||||||
|
|
||||||
from scrapy.addons import Addon
|
class MyAddon:
|
||||||
|
def update_settings(self, settings):
|
||||||
class MyAddon(Addon):
|
|
||||||
name = 'myaddon'
|
|
||||||
|
|
||||||
def update_settings(self, config, settings):
|
|
||||||
try:
|
try:
|
||||||
import boto
|
import boto
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise RuntimeError("myaddon requires the boto library")
|
raise RuntimeError("MyAddon requires the boto library")
|
||||||
super().update_settings(settings)
|
...
|
||||||
|
|
||||||
Check configuration of fully initialized crawler (see
|
|
||||||
:ref:`topics-api-crawler`)::
|
|
||||||
|
|
||||||
class MyAddon(object):
|
|
||||||
name = 'myaddon'
|
|
||||||
|
|
||||||
def update_settings(self, config, settings):
|
|
||||||
super().update_settings(settings)
|
|
||||||
settings.set('DNSCACHE_ENABLED', False, priority='addon')
|
|
||||||
|
|
||||||
def check_configuration(self, config, crawler):
|
|
||||||
if crawler.settings.getbool('DNSCACHE_ENABLED'):
|
|
||||||
# The spider, some other add-on, or the user messed with the
|
|
||||||
# DNS cache setting
|
|
||||||
raise ValueError("myaddon is incompatible with DNS cache")
|
|
||||||
|
|
|
||||||
124
scrapy/addons.py
124
scrapy/addons.py
|
|
@ -1,100 +1,16 @@
|
||||||
from typing import Any, Dict, Iterator, Mapping, Optional, OrderedDict
|
from typing import Any, List
|
||||||
|
|
||||||
from scrapy.utils.conf import build_component_list
|
from scrapy.utils.conf import build_component_list
|
||||||
from scrapy.utils.misc import load_object
|
from scrapy.utils.misc import load_object
|
||||||
|
|
||||||
|
|
||||||
class Addon(object):
|
class AddonManager:
|
||||||
name: str
|
"""This class facilitates loading and storing :ref:`topics-addons`."""
|
||||||
|
|
||||||
default_config = None
|
|
||||||
"""``dict`` with default configuration."""
|
|
||||||
|
|
||||||
config_mapping = None
|
|
||||||
"""``dict`` with mappings from config names to setting names. The given
|
|
||||||
setting names will be taken as given, not uppercased.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def export_config(self, config, settings):
|
|
||||||
"""Export the add-on configuration, all keys in caps, into the settings
|
|
||||||
object.
|
|
||||||
|
|
||||||
For example, the add-on configuration ``{'key': 'value'}`` will export
|
|
||||||
the setting ``KEY`` with a value of ``value``. All settings
|
|
||||||
will be exported with ``addon`` priority (see
|
|
||||||
:ref:`topics-api-settings`).
|
|
||||||
|
|
||||||
:param config: Add-on configuration to be exposed
|
|
||||||
:type config: ``dict``
|
|
||||||
|
|
||||||
:param settings: Settings object into which to export the configuration
|
|
||||||
:type settings: :class:`~scrapy.settings.Settings`
|
|
||||||
"""
|
|
||||||
conf = self.default_config or {}
|
|
||||||
conf.update(config)
|
|
||||||
# Since default exported config is case-insensitive (everything will be
|
|
||||||
# uppercased), make mapped config case-insensitive as well
|
|
||||||
conf_mapping = {k.lower(): v for k, v in (self.config_mapping or {}).items()}
|
|
||||||
for key, val in conf.items():
|
|
||||||
if key.lower() in conf_mapping:
|
|
||||||
key = conf_mapping[key.lower()]
|
|
||||||
else:
|
|
||||||
key = key.upper()
|
|
||||||
settings.set(key, val, "addon")
|
|
||||||
|
|
||||||
def update_settings(self, config, settings):
|
|
||||||
"""Modifiy `settings` to enable and configure required components.
|
|
||||||
|
|
||||||
:param config: Add-on configuration
|
|
||||||
:type config: ``dict``
|
|
||||||
|
|
||||||
:param settings: Crawler settings object
|
|
||||||
:type settings: :class:`~scrapy.settings.Settings`
|
|
||||||
"""
|
|
||||||
self.export_config(config, settings)
|
|
||||||
|
|
||||||
def check_configuration(self, config, crawler):
|
|
||||||
"""Perform post-initialization checks on fully configured `crawler`.
|
|
||||||
|
|
||||||
:param config: Add-on configuration
|
|
||||||
:type config: ``dict``
|
|
||||||
|
|
||||||
:param crawler: the fully-initialized crawler
|
|
||||||
:type crawler: :class:`~scrapy.crawler.Crawler`
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AddonManager(Mapping[str, Addon]):
|
|
||||||
"""This class facilitates loading and storing :ref:`topics-addons`.
|
|
||||||
|
|
||||||
You can treat it like a read-only dictionary in which keys correspond to
|
|
||||||
add-on names and values correspond to the add-on objects. Add-on
|
|
||||||
configurations are saved in the :attr:`config` dictionary attribute::
|
|
||||||
|
|
||||||
addons = AddonManager()
|
|
||||||
# ... load some add-ons here
|
|
||||||
print(addons.enabled) # prints names of all enabled add-ons
|
|
||||||
print(addons['TestAddon'].version) # prints version of add-on with name
|
|
||||||
# 'TestAddon'
|
|
||||||
print(addons.configs['TestAddon']) # prints configuration of 'TestAddon'
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._addons: OrderedDict[str, Addon] = OrderedDict[str, Addon]()
|
self.addons: List[Any] = []
|
||||||
self.configs: Dict[str, Dict[str, Any]] = {}
|
|
||||||
|
|
||||||
def __getitem__(self, name: str) -> Addon:
|
def add(self, addon: Any) -> None:
|
||||||
return self._addons[name]
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[str]:
|
|
||||||
return iter(self._addons)
|
|
||||||
|
|
||||||
def __len__(self) -> int:
|
|
||||||
return len(self._addons)
|
|
||||||
|
|
||||||
def add(self, addon: Any, config: Optional[Dict[str, Any]] = None):
|
|
||||||
"""Store an add-on.
|
"""Store an add-on.
|
||||||
|
|
||||||
:param addon: The add-on object (or path) to be stored
|
:param addon: The add-on object (or path) to be stored
|
||||||
|
|
@ -107,19 +23,13 @@ class AddonManager(Mapping[str, Addon]):
|
||||||
addon = load_object(addon)
|
addon = load_object(addon)
|
||||||
if isinstance(addon, type):
|
if isinstance(addon, type):
|
||||||
addon = addon()
|
addon = addon()
|
||||||
name = addon.name
|
self.addons.append(addon)
|
||||||
if name in self:
|
|
||||||
raise ValueError(f"Addon '{name}' already loaded")
|
|
||||||
self._addons[name] = addon
|
|
||||||
self.configs[name] = config or {}
|
|
||||||
|
|
||||||
def load_settings(self, settings):
|
def load_settings(self, settings) -> None:
|
||||||
"""Load add-ons and configurations from settings object.
|
"""Load add-ons and configurations from settings object.
|
||||||
|
|
||||||
This will load the addon for every add-on path in the
|
This will load the addon for every add-on path in the
|
||||||
``ADDONS`` setting. For each of these add-ons, the configuration will be
|
``ADDONS`` setting.
|
||||||
read from the dictionary setting whose name matches the uppercase add-on
|
|
||||||
name.
|
|
||||||
|
|
||||||
:param settings: The :class:`~scrapy.settings.Settings` object from \
|
:param settings: The :class:`~scrapy.settings.Settings` object from \
|
||||||
which to read the add-on configuration
|
which to read the add-on configuration
|
||||||
|
|
@ -127,9 +37,8 @@ class AddonManager(Mapping[str, Addon]):
|
||||||
"""
|
"""
|
||||||
paths = build_component_list(settings["ADDONS"])
|
paths = build_component_list(settings["ADDONS"])
|
||||||
addons = [load_object(path) for path in paths]
|
addons = [load_object(path) for path in paths]
|
||||||
configs = [settings.getdict(addon.name.upper()) for addon in addons]
|
for a in addons:
|
||||||
for a, c in zip(addons, configs):
|
self.add(a)
|
||||||
self.add(a, c)
|
|
||||||
|
|
||||||
def update_settings(self, settings) -> None:
|
def update_settings(self, settings) -> None:
|
||||||
"""Call ``update_settings()`` of all held add-ons.
|
"""Call ``update_settings()`` of all held add-ons.
|
||||||
|
|
@ -138,14 +47,5 @@ class AddonManager(Mapping[str, Addon]):
|
||||||
updated
|
updated
|
||||||
:type settings: :class:`~scrapy.settings.Settings`
|
:type settings: :class:`~scrapy.settings.Settings`
|
||||||
"""
|
"""
|
||||||
for name in self:
|
for addon in self.addons:
|
||||||
self[name].update_settings(self.configs[name], settings)
|
addon.update_settings(settings)
|
||||||
|
|
||||||
def check_configuration(self, crawler) -> None:
|
|
||||||
"""Call ``check_configuration()`` of all held add-ons.
|
|
||||||
|
|
||||||
:param crawler: the fully-initialized crawler
|
|
||||||
:type crawler: :class:`~scrapy.crawler.Crawler`
|
|
||||||
"""
|
|
||||||
for name in self:
|
|
||||||
self[name].check_configuration(self.configs[name], crawler)
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,29 @@
|
||||||
import unittest
|
import unittest
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from scrapy.addons import Addon, AddonManager
|
from scrapy.addons import AddonManager
|
||||||
from scrapy.settings import BaseSettings
|
from scrapy.settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
class GoodAddon(object):
|
class GoodAddon:
|
||||||
name = "GoodAddon"
|
name = "GoodAddon"
|
||||||
|
|
||||||
def update_settings(self, config, settings):
|
def __init__(self, config: Optional[Dict[str, Any]] = None) -> None:
|
||||||
pass
|
super().__init__()
|
||||||
|
self.config = config or {}
|
||||||
|
|
||||||
def check_configuration(self, config, crawler):
|
def update_settings(self, settings):
|
||||||
pass
|
settings.update(self.config, "addon")
|
||||||
|
|
||||||
|
|
||||||
class AddonTest(unittest.TestCase):
|
class AddonTest(unittest.TestCase):
|
||||||
def setUp(self):
|
|
||||||
class AddonWithAttributes(Addon):
|
|
||||||
name = "Test"
|
|
||||||
|
|
||||||
self.testaddon = AddonWithAttributes()
|
|
||||||
|
|
||||||
def test_export_config(self):
|
|
||||||
settings = BaseSettings()
|
|
||||||
self.testaddon.config_mapping = {"MAPPED_key": "MAPPING_WORKED"}
|
|
||||||
self.testaddon.default_config = {"key": 55, "defaultkey": 100}
|
|
||||||
self.testaddon.export_config(
|
|
||||||
{"key": 313, "OTHERKEY": True, "mapped_KEY": 99}, settings
|
|
||||||
)
|
|
||||||
self.assertEqual(settings["KEY"], 313)
|
|
||||||
self.assertEqual(settings["DEFAULTKEY"], 100)
|
|
||||||
self.assertEqual(settings["OTHERKEY"], True)
|
|
||||||
self.assertNotIn("MAPPED_key", settings)
|
|
||||||
self.assertNotIn("MAPPED_KEY", settings)
|
|
||||||
self.assertEqual(settings["MAPPING_WORKED"], 99)
|
|
||||||
self.assertEqual(settings.getpriority("KEY"), 15)
|
|
||||||
|
|
||||||
def test_update_settings(self):
|
def test_update_settings(self):
|
||||||
settings = BaseSettings()
|
settings = BaseSettings()
|
||||||
settings.set("KEY1", "default", priority="default")
|
settings.set("KEY1", "default", priority="default")
|
||||||
settings.set("KEY2", "project", priority="project")
|
settings.set("KEY2", "project", priority="project")
|
||||||
addon_config = {"key1": "addon", "key2": "addon", "key3": "addon"}
|
addon_config = {"KEY1": "addon", "KEY2": "addon", "KEY3": "addon"}
|
||||||
self.testaddon.update_settings(addon_config, settings)
|
testaddon = GoodAddon(addon_config)
|
||||||
|
testaddon.update_settings(settings)
|
||||||
self.assertEqual(settings["KEY1"], "addon")
|
self.assertEqual(settings["KEY1"], "addon")
|
||||||
self.assertEqual(settings["KEY2"], "project")
|
self.assertEqual(settings["KEY2"], "project")
|
||||||
self.assertEqual(settings["KEY3"], "addon")
|
self.assertEqual(settings["KEY3"], "addon")
|
||||||
|
|
@ -54,8 +36,7 @@ class AddonManagerTest(unittest.TestCase):
|
||||||
def test_add(self):
|
def test_add(self):
|
||||||
manager = AddonManager()
|
manager = AddonManager()
|
||||||
manager.add("tests.test_addons.GoodAddon")
|
manager.add("tests.test_addons.GoodAddon")
|
||||||
self.assertCountEqual(manager, ["GoodAddon"])
|
self.assertIsInstance(manager.addons[0], GoodAddon)
|
||||||
self.assertIsInstance(manager["GoodAddon"], GoodAddon)
|
|
||||||
|
|
||||||
def test_load_settings(self):
|
def test_load_settings(self):
|
||||||
settings = BaseSettings()
|
settings = BaseSettings()
|
||||||
|
|
@ -66,7 +47,4 @@ class AddonManagerTest(unittest.TestCase):
|
||||||
settings.set("GOODADDON", {"key": "val2"})
|
settings.set("GOODADDON", {"key": "val2"})
|
||||||
manager = AddonManager()
|
manager = AddonManager()
|
||||||
manager.load_settings(settings)
|
manager.load_settings(settings)
|
||||||
self.assertCountEqual(manager, ["GoodAddon"])
|
self.assertIsInstance(manager.addons[0], GoodAddon)
|
||||||
self.assertIsInstance(manager["GoodAddon"], GoodAddon)
|
|
||||||
self.assertCountEqual(manager.configs["GoodAddon"], ["key"])
|
|
||||||
self.assertEqual(manager.configs["GoodAddon"]["key"], "val2")
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ from twisted.python.failure import Failure
|
||||||
from twisted.trial.unittest import TestCase
|
from twisted.trial.unittest import TestCase
|
||||||
|
|
||||||
from scrapy import signals
|
from scrapy import signals
|
||||||
from scrapy.addons import Addon, AddonManager
|
|
||||||
from scrapy.crawler import CrawlerRunner
|
from scrapy.crawler import CrawlerRunner
|
||||||
from scrapy.exceptions import StopDownload
|
from scrapy.exceptions import StopDownload
|
||||||
from scrapy.http import Request
|
from scrapy.http import Request
|
||||||
|
|
@ -408,21 +407,6 @@ with multiples lines
|
||||||
self._assert_retried(log)
|
self._assert_retried(log)
|
||||||
self.assertIn("Got response 200", str(log))
|
self.assertIn("Got response 200", str(log))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def test_abort_on_addon_failed_check(self):
|
|
||||||
class FailedCheckAddon(Addon):
|
|
||||||
name = "FailedCheckAddon"
|
|
||||||
|
|
||||||
def check_configuration(self, config, crawler):
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
addonmgr = AddonManager()
|
|
||||||
addonmgr.add(FailedCheckAddon())
|
|
||||||
crawler = get_crawler(SimpleSpider)
|
|
||||||
crawler.addons = addonmgr
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
yield crawler.crawl()
|
|
||||||
|
|
||||||
|
|
||||||
class CrawlSpiderTestCase(TestCase):
|
class CrawlSpiderTestCase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue