Skip to content

Commit

Permalink
Allow passing classes directly in Settings
Browse files Browse the repository at this point in the history
Closes: #1215, #1032, #3870
Thanks
  • Loading branch information
nyov committed Mar 3, 2020
1 parent a4dbb77 commit eb80350
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 4 deletions.
27 changes: 27 additions & 0 deletions docs/topics/settings.rst
Expand Up @@ -16,6 +16,33 @@ project (in case you have many).

For a list of available built-in settings see: :ref:`topics-settings-ref`.

.. versionadded:: 2.0

When a setting references an object to be imported by Scrapy, until now
this had to be a string of an absolute (or project-wide) class path
(e.g. ``'myproject.extensions.TelnetConsole'``).

Now it is also possible to pass a class object (``TelnetConsole``) directly.
Passing a classname — to be instanced by Scrapy — is mostly useful in cases
without a Scrapy project environment, when writing Scrapy Crawlers in a
single Python source file, or in projects using Scrapy as a library.

Example::

from mybot.pipelines.validate import ValidateMyItem
ITEM_PIPELINES = {
# passing the classname...
ValidateMyItem: 300,
# ...equals passing the class path
'mybot.pipelines.validate.ValidateMyItem': 300,
}

Passing of already instanced objects is not supported.

.. note::
Directly passing a Scrapy class will circumvent Scrapy's
deprecation checks for that object.

.. _topics-settings-module-envvar:

Designating the settings
Expand Down
2 changes: 1 addition & 1 deletion scrapy/utils/deprecate.py
Expand Up @@ -131,7 +131,7 @@ def _clspath(cls, forced=None):
def update_classpath(path):
"""Update a deprecated path from an object with its new location"""
for prefix, replacement in DEPRECATION_RULES:
if path.startswith(prefix):
if isinstance(path, str) and path.startswith(prefix):
new_path = path.replace(prefix, replacement, 1)
warnings.warn("`{}` class is deprecated, use `{}` instead".format(path, new_path),
ScrapyDeprecationWarning)
Expand Down
13 changes: 11 additions & 2 deletions scrapy/utils/misc.py
Expand Up @@ -37,10 +37,19 @@ def arg_to_iter(arg):
def load_object(path):
"""Load an object given its absolute object path, and return it.
object can be the import path of a class, function, variable or an
instance, e.g. 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware'
The object can be the import path of a class, function, variable or an
instance, e.g. 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware'.
If ``path`` is not a string, but a callable() object, then return it as is.
"""

if not isinstance(path, str):
if callable(path):
return path
else:
raise TypeError("Unexpected argument type, expected string "
"or object, got: %s" % type(path))

try:
dot = path.rindex('.')
except ValueError:
Expand Down
33 changes: 33 additions & 0 deletions tests/test_settings/__init__.py
Expand Up @@ -382,6 +382,39 @@ def test_getdict_autodegrade_basesettings(self):
self.assertIn('key', mydict)
self.assertEqual(mydict['key'], 'val')

def test_passing_objects_as_values(self):
from scrapy.core.downloader.handlers.file import FileDownloadHandler
from scrapy.utils.misc import create_instance
from scrapy.utils.test import get_crawler

class TestPipeline():
def process_item(self, i, s):
return i

settings = Settings({
'ITEM_PIPELINES': {
TestPipeline: 800,
},
'DOWNLOAD_HANDLERS': {
'ftp': FileDownloadHandler,
},
})

self.assertIn('ITEM_PIPELINES', settings.attributes)

mypipeline, priority = settings.getdict('ITEM_PIPELINES').popitem()
self.assertEqual(priority, 800)
self.assertEqual(mypipeline, TestPipeline)
self.assertIsInstance(mypipeline(), TestPipeline)
self.assertEqual(mypipeline().process_item('item', None), 'item')

myhandler = settings.getdict('DOWNLOAD_HANDLERS').pop('ftp')
self.assertEqual(myhandler, FileDownloadHandler)
myhandler_instance = create_instance(myhandler, None, get_crawler())
self.assertIsInstance(myhandler_instance, FileDownloadHandler)
self.assertTrue(hasattr(myhandler_instance, 'download_request'))



if __name__ == "__main__":
unittest.main()
5 changes: 4 additions & 1 deletion tests/test_utils_misc/__init__.py
Expand Up @@ -13,10 +13,13 @@
class UtilsMiscTestCase(unittest.TestCase):

def test_load_object(self):
obj = load_object(load_object)
self.assertIs(obj, load_object)
obj = load_object('scrapy.utils.misc.load_object')
assert obj is load_object
self.assertIs(obj, load_object)
self.assertRaises(ImportError, load_object, 'nomodule999.mod.function')
self.assertRaises(NameError, load_object, 'scrapy.utils.misc.load_object999')
self.assertRaises(TypeError, load_object, dict())

def test_walk_modules(self):
mods = walk_modules('tests.test_utils_misc.test_walk_modules')
Expand Down

0 comments on commit eb80350

Please sign in to comment.