Merge branch 'master' of https://github.com/AKSHAYSHARMAJS/scrapy into integrate-mime

This commit is contained in:
Akshay Sharma 2022-05-08 17:48:57 -04:00
commit b2181d76d5
27 changed files with 848 additions and 94 deletions

View File

@ -20,7 +20,7 @@ jobs:
- python-version: pypy3
env:
TOXENV: pypy3
PYPY_VERSION: 3.6-v7.3.1
PYPY_VERSION: 3.6-v7.3.3
# pinned deps
- python-version: 3.6.12

View File

@ -350,6 +350,7 @@ I'm scraping a XML document and my XPath selector doesn't return any items
You may need to remove namespaces. See :ref:`removing-namespaces`.
.. _faq-split-item:
How to split an item into multiple items in an item pipeline?
@ -406,8 +407,19 @@ or :class:`~scrapy.signals.headers_received` signals and raising a
:ref:`topics-stop-response-download` topic for additional information and examples.
Running ``runspider`` I get ``error: No spider found in file: <filename>``
--------------------------------------------------------------------------
This may happen if your Scrapy project has a spider module with a name that
conflicts with the name of one of the `Python standard library modules`_, such
as ``csv.py`` or ``os.py``, or any `Python package`_ that you have installed.
See :issue:`2680`.
.. _has been reported: https://github.com/scrapy/scrapy/issues/2905
.. _Python standard library modules: https://docs.python.org/py-modindex.html
.. _Python package: https://pypi.org/
.. _user agents: https://en.wikipedia.org/wiki/User_agent
.. _LIFO: https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
.. _DFO order: https://en.wikipedia.org/wiki/Depth-first_search
.. _BFO order: https://en.wikipedia.org/wiki/Breadth-first_search
.. _BFO order: https://en.wikipedia.org/wiki/Breadth-first_search

View File

@ -1070,9 +1070,13 @@ In order to use this parser:
* Install `Reppy <https://github.com/seomoz/reppy/>`_ by running ``pip install reppy``
.. warning:: `Upstream issue #122
<https://github.com/seomoz/reppy/issues/122>`_ prevents reppy usage in Python 3.9+.
* Set :setting:`ROBOTSTXT_PARSER` setting to
``scrapy.robotstxt.ReppyRobotParser``
.. _rerp-parser:
Robotexclusionrulesparser

View File

@ -272,6 +272,7 @@ in multiple files, with the specified maximum item count per file. That way, as
soon as a file reaches the maximum item count, that file is delivered to the
feed URI, allowing item delivery to start way before the end of the crawl.
.. _item-filter:
Item filtering
@ -312,6 +313,63 @@ ItemFilter
:members:
.. _post-processing:
Post-Processing
===============
.. versionadded:: VERSION
Scrapy provides an option to activate plugins to post-process feeds before they are exported
to feed storages. In addition to using :ref:`builtin plugins <builtin-plugins>`, you
can create your own :ref:`plugins <custom-plugins>`.
These plugins can be activated through the ``postprocessing`` option of a feed.
The option must be passed a list of post-processing plugins in the order you want
the feed to be processed. These plugins can be declared either as an import string
or with the imported class of the plugin. Parameters to plugins can be passed
through the feed options. See :ref:`feed options <feed-options>` for examples.
.. _builtin-plugins:
Built-in Plugins
----------------
.. autoclass:: scrapy.extensions.postprocessing.GzipPlugin
.. autoclass:: scrapy.extensions.postprocessing.LZMAPlugin
.. autoclass:: scrapy.extensions.postprocessing.Bz2Plugin
.. _custom-plugins:
Custom Plugins
--------------
Each plugin is a class that must implement the following methods:
.. method:: __init__(self, file, feed_options)
Initialize the plugin.
:param file: file-like object having at least the `write`, `tell` and `close` methods implemented
:param feed_options: feed-specific :ref:`options <feed-options>`
:type feed_options: :class:`dict`
.. method:: write(self, data)
Process and write `data` (:class:`bytes` or :class:`memoryview`) into the plugin's target file.
It must return number of bytes written.
.. method:: close(self)
Close the target file object.
To pass a parameter to your plugin, use :ref:`feed options <feed-options>`. You
can then access those parameters from the ``__init__`` method of your plugin.
Settings
========
@ -368,10 +426,12 @@ For instance::
'encoding': 'latin1',
'indent': 8,
},
pathlib.Path('items.csv'): {
pathlib.Path('items.csv.gz'): {
'format': 'csv',
'fields': ['price', 'name'],
'item_filter': 'myproject.filters.MyCustomFilter2',
'postprocessing': [MyPlugin1, 'scrapy.extensions.postprocessing.GzipPlugin'],
'gzip_compresslevel': 5,
},
}
@ -435,6 +495,11 @@ as a fallback value if that key is not provided for a specific feed definition:
- ``uri_params``: falls back to :setting:`FEED_URI_PARAMS`.
- ``postprocessing``: list of :ref:`plugins <post-processing>` to use for post-processing.
The plugins will be used in the order of the list passed.
.. versionadded:: VERSION
.. setting:: FEED_EXPORT_ENCODING

View File

@ -989,6 +989,16 @@ Default: ``{}``
A dict containing the pipelines enabled by default in Scrapy. You should never
modify this setting in your project, modify :setting:`ITEM_PIPELINES` instead.
.. setting:: JOBDIR
JOBDIR
------
Default: ``''``
A string indicating the directory for storing the state of a crawl when
:ref:`pausing and resuming crawls <topics-jobs>`.
.. setting:: LOG_ENABLED
LOG_ENABLED

View File

@ -105,6 +105,7 @@ disable=abstract-method,
unnecessary-lambda,
unnecessary-pass,
unreachable,
unspecified-encoding,
unsubscriptable-object,
unused-argument,
unused-import,

View File

@ -4,6 +4,7 @@ import string
from importlib import import_module
from os.path import join, dirname, abspath, exists, splitext
from urllib.parse import urlparse
import scrapy
from scrapy.commands import ScrapyCommand
@ -22,6 +23,14 @@ def sanitize_module_name(module_name):
return module_name
def extract_domain(url):
"""Extract domain name from URL string"""
o = urlparse(url)
if o.scheme == '' and o.netloc == '':
o = urlparse("//" + url.lstrip("/"))
return o.netloc
class Command(ScrapyCommand):
requires_project = False
@ -59,7 +68,8 @@ class Command(ScrapyCommand):
if len(args) != 2:
raise UsageError()
name, domain = args[0:2]
name, url = args[0:2]
domain = extract_domain(url)
module = sanitize_module_name(name)
if self.settings.get('BOT_NAME') == module:

View File

@ -1,7 +1,7 @@
import re
import os
import string
from importlib import import_module
from importlib.util import find_spec
from os.path import join, exists, abspath
from shutil import ignore_patterns, move, copy2, copystat
from stat import S_IWUSR as OWNER_WRITE_PERMISSION
@ -42,11 +42,8 @@ class Command(ScrapyCommand):
def _is_valid_name(self, project_name):
def _module_exists(module_name):
try:
import_module(module_name)
return True
except ImportError:
return False
spec = find_spec(module_name)
return spec is not None and spec.loader is not None
if not re.search(r'^[_a-zA-Z]\w*$', project_name):
print('Error: Project names must begin with a letter and contain'

View File

@ -20,6 +20,7 @@ from zope.interface import implementer, Interface
from scrapy import signals
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
from scrapy.extensions.postprocessing import PostProcessingManager
from scrapy.utils.boto import is_botocore_available
from scrapy.utils.conf import feed_complete_default_values_from_settings
from scrapy.utils.ftp import ftp_store_file
@ -396,6 +397,9 @@ class FeedExporter:
"""
storage = self._get_storage(uri, feed_options)
file = storage.open(spider)
if "postprocessing" in feed_options:
file = PostProcessingManager(feed_options["postprocessing"], file, feed_options)
exporter = self._get_exporter(
file=file,
format=feed_options['format'],

View File

@ -0,0 +1,154 @@
"""
Extension for processing data before they are exported to feeds.
"""
from bz2 import BZ2File
from gzip import GzipFile
from io import IOBase
from lzma import LZMAFile
from typing import Any, BinaryIO, Dict, List
from scrapy.utils.misc import load_object
class GzipPlugin:
"""
Compresses received data using `gzip <https://en.wikipedia.org/wiki/Gzip>`_.
Accepted ``feed_options`` parameters:
- `gzip_compresslevel`
- `gzip_mtime`
- `gzip_filename`
See :py:class:`gzip.GzipFile` for more info about parameters.
"""
def __init__(self, file: BinaryIO, feed_options: Dict[str, Any]) -> None:
self.file = file
self.feed_options = feed_options
compress_level = self.feed_options.get("gzip_compresslevel", 9)
mtime = self.feed_options.get("gzip_mtime")
filename = self.feed_options.get("gzip_filename")
self.gzipfile = GzipFile(fileobj=self.file, mode="wb", compresslevel=compress_level,
mtime=mtime, filename=filename)
def write(self, data: bytes) -> int:
return self.gzipfile.write(data)
def close(self) -> None:
self.gzipfile.close()
self.file.close()
class Bz2Plugin:
"""
Compresses received data using `bz2 <https://en.wikipedia.org/wiki/Bzip2>`_.
Accepted ``feed_options`` parameters:
- `bz2_compresslevel`
See :py:class:`bz2.BZ2File` for more info about parameters.
"""
def __init__(self, file: BinaryIO, feed_options: Dict[str, Any]) -> None:
self.file = file
self.feed_options = feed_options
compress_level = self.feed_options.get("bz2_compresslevel", 9)
self.bz2file = BZ2File(filename=self.file, mode="wb", compresslevel=compress_level)
def write(self, data: bytes) -> int:
return self.bz2file.write(data)
def close(self) -> None:
self.bz2file.close()
self.file.close()
class LZMAPlugin:
"""
Compresses received data using `lzma <https://en.wikipedia.org/wiki/LempelZivMarkov_chain_algorithm>`_.
Accepted ``feed_options`` parameters:
- `lzma_format`
- `lzma_check`
- `lzma_preset`
- `lzma_filters`
.. note::
``lzma_filters`` cannot be used in pypy version 7.3.1 and older.
See :py:class:`lzma.LZMAFile` for more info about parameters.
"""
def __init__(self, file: BinaryIO, feed_options: Dict[str, Any]) -> None:
self.file = file
self.feed_options = feed_options
format = self.feed_options.get("lzma_format")
check = self.feed_options.get("lzma_check", -1)
preset = self.feed_options.get("lzma_preset")
filters = self.feed_options.get("lzma_filters")
self.lzmafile = LZMAFile(filename=self.file, mode="wb", format=format,
check=check, preset=preset, filters=filters)
def write(self, data: bytes) -> int:
return self.lzmafile.write(data)
def close(self) -> None:
self.lzmafile.close()
self.file.close()
# io.IOBase is subclassed here, so that exporters can use the PostProcessingManager
# instance as a file like writable object. This could be needed by some exporters
# such as CsvItemExporter which wraps the feed storage with io.TextIOWrapper.
class PostProcessingManager(IOBase):
"""
This will manage and use declared plugins to process data in a
pipeline-ish way.
:param plugins: all the declared plugins for the feed
:type plugins: list
:param file: final target file where the processed data will be written
:type file: file like object
"""
def __init__(self, plugins: List[Any], file: BinaryIO, feed_options: Dict[str, Any]) -> None:
self.plugins = self._load_plugins(plugins)
self.file = file
self.feed_options = feed_options
self.head_plugin = self._get_head_plugin()
def write(self, data: bytes) -> int:
"""
Uses all the declared plugins to process data first, then writes
the processed data to target file.
:param data: data passed to be written to target file
:type data: bytes
:return: returns number of bytes written
:rtype: int
"""
return self.head_plugin.write(data)
def tell(self) -> int:
return self.file.tell()
def close(self) -> None:
"""
Close the target file along with all the plugins.
"""
self.head_plugin.close()
def writable(self) -> bool:
return True
def _load_plugins(self, plugins: List[Any]) -> List[Any]:
plugins = [load_object(plugin) for plugin in plugins]
return plugins
def _get_head_plugin(self) -> Any:
prev = self.file
for plugin in self.plugins[::-1]:
prev = plugin(prev, self.feed_options)
return prev

View File

@ -4,14 +4,12 @@ Base class for Scrapy spiders
See documentation in docs/topics/spiders.rst
"""
import logging
import warnings
from typing import Optional
from scrapy import signals
from scrapy.http import Request
from scrapy.utils.trackref import object_ref
from scrapy.utils.url import url_is_from_spider
from scrapy.utils.deprecate import method_is_overridden
class Spider(object_ref):
@ -57,34 +55,13 @@ class Spider(object_ref):
crawler.signals.connect(self.close, signals.spider_closed)
def start_requests(self):
cls = self.__class__
if not self.start_urls and hasattr(self, 'start_url'):
raise AttributeError(
"Crawling could not start: 'start_urls' not found "
"or empty (but found 'start_url' attribute instead, "
"did you miss an 's'?)")
if method_is_overridden(cls, Spider, 'make_requests_from_url'):
warnings.warn(
"Spider.make_requests_from_url method is deprecated; it "
"won't be called in future Scrapy releases. Please "
"override Spider.start_requests method instead "
f"(see {cls.__module__}.{cls.__name__}).",
)
for url in self.start_urls:
yield self.make_requests_from_url(url)
else:
for url in self.start_urls:
yield Request(url, dont_filter=True)
def make_requests_from_url(self, url):
""" This method is deprecated. """
warnings.warn(
"Spider.make_requests_from_url method is deprecated: "
"it will be removed and not be called by the default "
"Spider.start_requests method in future Scrapy releases. "
"Please override Spider.start_requests method instead."
)
return Request(url, dont_filter=True)
for url in self.start_urls:
yield Request(url, dont_filter=True)
def _parse(self, response, **kwargs):
return self.parse(response, **kwargs)

View File

@ -121,7 +121,7 @@ def feed_complete_default_values_from_settings(feed, settings):
out.setdefault("fields", settings.getlist("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", dict())
out.setdefault("item_export_kwargs", {})
if settings["FEED_EXPORT_INDENT"] is None:
out.setdefault("indent", None)
else:

View File

@ -79,7 +79,7 @@ def create_deprecated_class(
# for implementation details
def __instancecheck__(cls, inst):
return any(cls.__subclasscheck__(c)
for c in {type(inst), inst.__class__})
for c in (type(inst), inst.__class__))
def __subclasscheck__(cls, sub):
if cls is not DeprecatedClass.deprecated_class:

View File

@ -40,9 +40,9 @@ def generate_keys():
subject = issuer = Name(
[
NameAttribute(NameOID.COUNTRY_NAME, u"IE"),
NameAttribute(NameOID.ORGANIZATION_NAME, u"Scrapy"),
NameAttribute(NameOID.COMMON_NAME, u"localhost"),
NameAttribute(NameOID.COUNTRY_NAME, "IE"),
NameAttribute(NameOID.ORGANIZATION_NAME, "Scrapy"),
NameAttribute(NameOID.COMMON_NAME, "localhost"),
]
)
cert = (
@ -54,7 +54,7 @@ def generate_keys():
.not_valid_before(datetime.utcnow())
.not_valid_after(datetime.utcnow() + timedelta(days=10))
.add_extension(
SubjectAlternativeName([DNSName(u"localhost")]),
SubjectAlternativeName([DNSName("localhost")]),
critical=False,
)
.sign(key, SHA256(), default_backend())

View File

@ -3,6 +3,7 @@ import json
import optparse
import os
import platform
import re
import subprocess
import sys
import tempfile
@ -71,9 +72,14 @@ class ProjectTest(unittest.TestCase):
def proc(self, *new_args, **popen_kwargs):
args = (sys.executable, '-m', 'scrapy.cmdline') + new_args
p = subprocess.Popen(args, cwd=self.cwd, env=self.env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
**popen_kwargs)
p = subprocess.Popen(
args,
cwd=popen_kwargs.pop('cwd', self.cwd),
env=self.env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
**popen_kwargs,
)
def kill_proc():
p.kill()
@ -89,11 +95,23 @@ class ProjectTest(unittest.TestCase):
return p, to_unicode(stdout), to_unicode(stderr)
def find_in_file(self, filename, regex):
"""Find first pattern occurrence in file"""
pattern = re.compile(regex)
with open(filename, "r") as f:
for line in f:
match = pattern.search(line)
if match is not None:
return match
class StartprojectTest(ProjectTest):
def test_startproject(self):
self.assertEqual(0, self.call('startproject', self.project_name))
p, out, err = self.proc('startproject', self.project_name)
print(out)
print(err, file=sys.stderr)
self.assertEqual(p.returncode, 0)
assert exists(join(self.proj_path, 'scrapy.cfg'))
assert exists(join(self.proj_path, 'testproject'))
@ -128,6 +146,25 @@ class StartprojectTest(ProjectTest):
self.assertEqual(2, self.call('startproject'))
self.assertEqual(2, self.call('startproject', self.project_name, project_dir, 'another_params'))
def test_existing_project_dir(self):
project_dir = mkdtemp()
project_name = self.project_name + '_existing'
project_path = os.path.join(project_dir, project_name)
os.mkdir(project_path)
p, out, err = self.proc('startproject', project_name, cwd=project_dir)
print(out)
print(err, file=sys.stderr)
self.assertEqual(p.returncode, 0)
assert exists(join(abspath(project_path), 'scrapy.cfg'))
assert exists(join(abspath(project_path), project_name))
assert exists(join(join(abspath(project_path), project_name), '__init__.py'))
assert exists(join(join(abspath(project_path), project_name), 'items.py'))
assert exists(join(join(abspath(project_path), project_name), 'pipelines.py'))
assert exists(join(join(abspath(project_path), project_name), 'settings.py'))
assert exists(join(join(abspath(project_path), project_name), 'spiders', '__init__.py'))
def get_permissions_dict(path, renamings=None, ignore=None):
@ -455,6 +492,26 @@ class GenspiderCommandTest(CommandTest):
def test_same_filename_as_existing_spider_force(self):
self.test_same_filename_as_existing_spider(force=True)
def test_url(self, url='test.com', domain="test.com"):
self.assertEqual(0, self.call('genspider', '--force', 'test_name', url))
self.assertEqual(domain,
self.find_in_file(join(self.proj_mod_path,
'spiders', 'test_name.py'),
r'allowed_domains\s*=\s*\[\'(.+)\'\]').group(1))
self.assertEqual('http://%s/' % domain,
self.find_in_file(join(self.proj_mod_path,
'spiders', 'test_name.py'),
r'start_urls\s*=\s*\[\'(.+)\'\]').group(1))
def test_url_schema(self):
self.test_url('http://test.com', 'test.com')
def test_url_path(self):
self.test_url('test.com/some/other/page', 'test.com')
def test_url_schema_path(self):
self.test_url('https://test.com/some/other/page', 'test.com')
class GenspiderStandaloneCommandTest(ProjectTest):

View File

@ -219,7 +219,7 @@ class Https2ProxyTestCase(Http11ProxyTestCase):
certfile = 'keys/localhost.crt'
scheme = 'https'
host = u'127.0.0.1'
host = '127.0.0.1'
expected_http_proxy_request_body = b'/'

View File

@ -152,7 +152,7 @@ class CrawlerRun:
self.itemerror = []
self.itemresp = []
self.headers = {}
self.bytes = defaultdict(lambda: list())
self.bytes = defaultdict(list)
self.signals_caught = {}
self.spider_class = spider_class

View File

@ -362,14 +362,14 @@ class CsvItemExporterTest(BaseItemExporterTest):
def test_errors_default(self):
with self.assertRaises(UnicodeEncodeError):
self.assertExportResult(
item=dict(text=u'W\u0275\u200Brd'),
item=dict(text='W\u0275\u200Brd'),
expected=None,
encoding='windows-1251',
)
def test_errors_xmlcharrefreplace(self):
self.assertExportResult(
item=dict(text=u'W\u0275\u200Brd'),
item=dict(text='W\u0275\u200Brd'),
include_headers_line=False,
expected='W&#629;&#8203;rd\r\n',
encoding='windows-1251',

View File

@ -1,5 +1,8 @@
import bz2
import csv
import gzip
import json
import lzma
import os
import random
import shutil
@ -1473,6 +1476,499 @@ class FeedExportTest(FeedExportTestBase):
self.assertEqual(row['expected'], data[feed_options['format']])
class FeedPostProcessedExportsTest(FeedExportTestBase):
__test__ = True
items = [{'foo': 'bar'}]
expected = b'foo\r\nbar\r\n'
class MyPlugin1:
def __init__(self, file, feed_options):
self.file = file
self.feed_options = feed_options
self.char = self.feed_options.get('plugin1_char', b'')
def write(self, data):
written_count = self.file.write(data)
written_count += self.file.write(self.char)
return written_count
def close(self):
self.file.close()
def _named_tempfile(self, name):
return os.path.join(self.temp_dir, name)
@defer.inlineCallbacks
def run_and_export(self, spider_cls, settings):
""" Run spider with specified settings; return exported data with filename. """
FEEDS = settings.get('FEEDS') or {}
settings['FEEDS'] = {
printf_escape(path_to_url(file_path)): feed_options
for file_path, feed_options in FEEDS.items()
}
content = {}
try:
with MockServer() as s:
runner = CrawlerRunner(Settings(settings))
spider_cls.start_urls = [s.url('/')]
yield runner.crawl(spider_cls)
for file_path, feed_options in FEEDS.items():
if not os.path.exists(str(file_path)):
continue
with open(str(file_path), 'rb') as f:
content[str(file_path)] = f.read()
finally:
for file_path in FEEDS.keys():
if not os.path.exists(str(file_path)):
continue
os.remove(str(file_path))
return content
def get_gzip_compressed(self, data, compresslevel=9, mtime=0, filename=''):
data_stream = BytesIO()
gzipf = gzip.GzipFile(fileobj=data_stream, filename=filename, mtime=mtime,
compresslevel=compresslevel, mode="wb")
gzipf.write(data)
gzipf.close()
data_stream.seek(0)
return data_stream.read()
@defer.inlineCallbacks
def test_gzip_plugin(self):
filename = self._named_tempfile('gzip_file')
settings = {
'FEEDS': {
filename: {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.GzipPlugin'],
},
},
}
data = yield self.exported_data(self.items, settings)
try:
gzip.decompress(data[filename])
except OSError:
self.fail("Received invalid gzip data.")
@defer.inlineCallbacks
def test_gzip_plugin_compresslevel(self):
filename_to_compressed = {
self._named_tempfile('compresslevel_0'): self.get_gzip_compressed(self.expected, compresslevel=0),
self._named_tempfile('compresslevel_9'): self.get_gzip_compressed(self.expected, compresslevel=9),
}
settings = {
'FEEDS': {
self._named_tempfile('compresslevel_0'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.GzipPlugin'],
'gzip_compresslevel': 0,
'gzip_mtime': 0,
'gzip_filename': "",
},
self._named_tempfile('compresslevel_9'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.GzipPlugin'],
'gzip_compresslevel': 9,
'gzip_mtime': 0,
'gzip_filename': "",
},
},
}
data = yield self.exported_data(self.items, settings)
for filename, compressed in filename_to_compressed.items():
result = gzip.decompress(data[filename])
self.assertEqual(compressed, data[filename])
self.assertEqual(self.expected, result)
@defer.inlineCallbacks
def test_gzip_plugin_mtime(self):
filename_to_compressed = {
self._named_tempfile('mtime_123'): self.get_gzip_compressed(self.expected, mtime=123),
self._named_tempfile('mtime_123456789'): self.get_gzip_compressed(self.expected, mtime=123456789),
}
settings = {
'FEEDS': {
self._named_tempfile('mtime_123'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.GzipPlugin'],
'gzip_mtime': 123,
'gzip_filename': "",
},
self._named_tempfile('mtime_123456789'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.GzipPlugin'],
'gzip_mtime': 123456789,
'gzip_filename': "",
},
},
}
data = yield self.exported_data(self.items, settings)
for filename, compressed in filename_to_compressed.items():
result = gzip.decompress(data[filename])
self.assertEqual(compressed, data[filename])
self.assertEqual(self.expected, result)
@defer.inlineCallbacks
def test_gzip_plugin_filename(self):
filename_to_compressed = {
self._named_tempfile('filename_FILE1'): self.get_gzip_compressed(self.expected, filename="FILE1"),
self._named_tempfile('filename_FILE2'): self.get_gzip_compressed(self.expected, filename="FILE2"),
}
settings = {
'FEEDS': {
self._named_tempfile('filename_FILE1'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.GzipPlugin'],
'gzip_mtime': 0,
'gzip_filename': "FILE1",
},
self._named_tempfile('filename_FILE2'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.GzipPlugin'],
'gzip_mtime': 0,
'gzip_filename': "FILE2",
},
},
}
data = yield self.exported_data(self.items, settings)
for filename, compressed in filename_to_compressed.items():
result = gzip.decompress(data[filename])
self.assertEqual(compressed, data[filename])
self.assertEqual(self.expected, result)
@defer.inlineCallbacks
def test_lzma_plugin(self):
filename = self._named_tempfile('lzma_file')
settings = {
'FEEDS': {
filename: {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.LZMAPlugin'],
},
},
}
data = yield self.exported_data(self.items, settings)
try:
lzma.decompress(data[filename])
except lzma.LZMAError:
self.fail("Received invalid lzma data.")
@defer.inlineCallbacks
def test_lzma_plugin_format(self):
filename_to_compressed = {
self._named_tempfile('format_FORMAT_XZ'): lzma.compress(self.expected, format=lzma.FORMAT_XZ),
self._named_tempfile('format_FORMAT_ALONE'): lzma.compress(self.expected, format=lzma.FORMAT_ALONE),
}
settings = {
'FEEDS': {
self._named_tempfile('format_FORMAT_XZ'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.LZMAPlugin'],
'lzma_format': lzma.FORMAT_XZ,
},
self._named_tempfile('format_FORMAT_ALONE'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.LZMAPlugin'],
'lzma_format': lzma.FORMAT_ALONE,
},
},
}
data = yield self.exported_data(self.items, settings)
for filename, compressed in filename_to_compressed.items():
result = lzma.decompress(data[filename])
self.assertEqual(compressed, data[filename])
self.assertEqual(self.expected, result)
@defer.inlineCallbacks
def test_lzma_plugin_check(self):
filename_to_compressed = {
self._named_tempfile('check_CHECK_NONE'): lzma.compress(self.expected, check=lzma.CHECK_NONE),
self._named_tempfile('check_CHECK_CRC256'): lzma.compress(self.expected, check=lzma.CHECK_SHA256),
}
settings = {
'FEEDS': {
self._named_tempfile('check_CHECK_NONE'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.LZMAPlugin'],
'lzma_check': lzma.CHECK_NONE,
},
self._named_tempfile('check_CHECK_CRC256'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.LZMAPlugin'],
'lzma_check': lzma.CHECK_SHA256,
},
},
}
data = yield self.exported_data(self.items, settings)
for filename, compressed in filename_to_compressed.items():
result = lzma.decompress(data[filename])
self.assertEqual(compressed, data[filename])
self.assertEqual(self.expected, result)
@defer.inlineCallbacks
def test_lzma_plugin_preset(self):
filename_to_compressed = {
self._named_tempfile('preset_PRESET_0'): lzma.compress(self.expected, preset=0),
self._named_tempfile('preset_PRESET_9'): lzma.compress(self.expected, preset=9),
}
settings = {
'FEEDS': {
self._named_tempfile('preset_PRESET_0'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.LZMAPlugin'],
'lzma_preset': 0,
},
self._named_tempfile('preset_PRESET_9'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.LZMAPlugin'],
'lzma_preset': 9,
},
},
}
data = yield self.exported_data(self.items, settings)
for filename, compressed in filename_to_compressed.items():
result = lzma.decompress(data[filename])
self.assertEqual(compressed, data[filename])
self.assertEqual(self.expected, result)
@defer.inlineCallbacks
def test_lzma_plugin_filters(self):
import sys
if "PyPy" in sys.version:
# https://foss.heptapod.net/pypy/pypy/-/issues/3527
raise unittest.SkipTest("lzma filters doesn't work in PyPy")
filters = [{'id': lzma.FILTER_LZMA2}]
compressed = lzma.compress(self.expected, filters=filters)
filename = self._named_tempfile('filters')
settings = {
'FEEDS': {
filename: {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.LZMAPlugin'],
'lzma_filters': filters,
},
},
}
data = yield self.exported_data(self.items, settings)
self.assertEqual(compressed, data[filename])
result = lzma.decompress(data[filename])
self.assertEqual(self.expected, result)
@defer.inlineCallbacks
def test_bz2_plugin(self):
filename = self._named_tempfile('bz2_file')
settings = {
'FEEDS': {
filename: {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.Bz2Plugin'],
},
},
}
data = yield self.exported_data(self.items, settings)
try:
bz2.decompress(data[filename])
except OSError:
self.fail("Received invalid bz2 data.")
@defer.inlineCallbacks
def test_bz2_plugin_compresslevel(self):
filename_to_compressed = {
self._named_tempfile('compresslevel_1'): bz2.compress(self.expected, compresslevel=1),
self._named_tempfile('compresslevel_9'): bz2.compress(self.expected, compresslevel=9),
}
settings = {
'FEEDS': {
self._named_tempfile('compresslevel_1'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.Bz2Plugin'],
'bz2_compresslevel': 1,
},
self._named_tempfile('compresslevel_9'): {
'format': 'csv',
'postprocessing': ['scrapy.extensions.postprocessing.Bz2Plugin'],
'bz2_compresslevel': 9,
},
},
}
data = yield self.exported_data(self.items, settings)
for filename, compressed in filename_to_compressed.items():
result = bz2.decompress(data[filename])
self.assertEqual(compressed, data[filename])
self.assertEqual(self.expected, result)
@defer.inlineCallbacks
def test_custom_plugin(self):
filename = self._named_tempfile('csv_file')
settings = {
'FEEDS': {
filename: {
'format': 'csv',
'postprocessing': [self.MyPlugin1],
},
},
}
data = yield self.exported_data(self.items, settings)
self.assertEqual(self.expected, data[filename])
@defer.inlineCallbacks
def test_custom_plugin_with_parameter(self):
expected = b'foo\r\n\nbar\r\n\n'
filename = self._named_tempfile('newline')
settings = {
'FEEDS': {
filename: {
'format': 'csv',
'postprocessing': [self.MyPlugin1],
'plugin1_char': b'\n'
},
},
}
data = yield self.exported_data(self.items, settings)
self.assertEqual(expected, data[filename])
@defer.inlineCallbacks
def test_custom_plugin_with_compression(self):
expected = b'foo\r\n\nbar\r\n\n'
filename_to_decompressor = {
self._named_tempfile('bz2'): bz2.decompress,
self._named_tempfile('lzma'): lzma.decompress,
self._named_tempfile('gzip'): gzip.decompress,
}
settings = {
'FEEDS': {
self._named_tempfile('bz2'): {
'format': 'csv',
'postprocessing': [self.MyPlugin1, 'scrapy.extensions.postprocessing.Bz2Plugin'],
'plugin1_char': b'\n',
},
self._named_tempfile('lzma'): {
'format': 'csv',
'postprocessing': [self.MyPlugin1, 'scrapy.extensions.postprocessing.LZMAPlugin'],
'plugin1_char': b'\n',
},
self._named_tempfile('gzip'): {
'format': 'csv',
'postprocessing': [self.MyPlugin1, 'scrapy.extensions.postprocessing.GzipPlugin'],
'plugin1_char': b'\n',
},
},
}
data = yield self.exported_data(self.items, settings)
for filename, decompressor in filename_to_decompressor.items():
result = decompressor(data[filename])
self.assertEqual(expected, result)
@defer.inlineCallbacks
def test_exports_compatibility_with_postproc(self):
import marshal
import pickle
filename_to_expected = {
self._named_tempfile('csv'): b'foo\r\nbar\r\n',
self._named_tempfile('json'): b'[\n{"foo": "bar"}\n]',
self._named_tempfile('jsonlines'): b'{"foo": "bar"}\n',
self._named_tempfile('xml'): b'<?xml version="1.0" encoding="utf-8"?>\n'
b'<items>\n<item><foo>bar</foo></item>\n</items>',
}
settings = {
'FEEDS': {
self._named_tempfile('csv'): {
'format': 'csv',
'postprocessing': [self.MyPlugin1],
# empty plugin to activate postprocessing.PostProcessingManager
},
self._named_tempfile('json'): {
'format': 'json',
'postprocessing': [self.MyPlugin1],
},
self._named_tempfile('jsonlines'): {
'format': 'jsonlines',
'postprocessing': [self.MyPlugin1],
},
self._named_tempfile('xml'): {
'format': 'xml',
'postprocessing': [self.MyPlugin1],
},
self._named_tempfile('marshal'): {
'format': 'marshal',
'postprocessing': [self.MyPlugin1],
},
self._named_tempfile('pickle'): {
'format': 'pickle',
'postprocessing': [self.MyPlugin1],
},
},
}
data = yield self.exported_data(self.items, settings)
for filename, result in data.items():
if 'pickle' in filename:
expected, result = self.items[0], pickle.loads(result)
elif 'marshal' in filename:
expected, result = self.items[0], marshal.loads(result)
else:
expected = filename_to_expected[filename]
self.assertEqual(expected, result)
class BatchDeliveriesTest(FeedExportTestBase):
__test__ = True
_file_mark = '_%(batch_time)s_#%(batch_id)02d_'

View File

@ -201,7 +201,7 @@ class Https2ClientProtocolTestCase(TestCase):
self.site = Site(root, timeout=None)
# Start server for testing
self.hostname = u'localhost'
self.hostname = 'localhost'
context_factory = ssl_context_factory(self.key_file, self.certificate_file)
server_endpoint = SSL4ServerEndpoint(reactor, 0, context_factory, interface=self.hostname)

View File

@ -703,7 +703,7 @@ class DeprecatedUtilityFunctionsTestCase(unittest.TestCase):
return None
with warnings.catch_warnings(record=True) as w:
wrap_loader_context(function, context=dict())
wrap_loader_context(function, context={})
assert len(w) == 1
assert issubclass(w[0].category, ScrapyDeprecationWarning)

View File

@ -57,7 +57,7 @@ class KeywordArgumentsSpider(MockServerSpider):
},
}
checks = list()
checks = []
def start_requests(self):
data = {'key': 'value', 'number': 123}

View File

@ -22,7 +22,7 @@ MockSlot = collections.namedtuple('MockSlot', ['active'])
class MockDownloader:
def __init__(self):
self.slots = dict()
self.slots = {}
def _get_slot_key(self, request, spider):
if Downloader.DOWNLOAD_SLOT in request.meta:
@ -31,7 +31,7 @@ class MockDownloader:
return urlparse_cached(request).hostname or ''
def increment(self, slot_key):
slot = self.slots.setdefault(slot_key, MockSlot(active=list()))
slot = self.slots.setdefault(slot_key, MockSlot(active=[]))
slot.active.append(1)
def decrement(self, slot_key):
@ -114,7 +114,7 @@ class BaseSchedulerInMemoryTester(SchedulerHandler):
for url, priority in _PRIORITIES:
self.scheduler.enqueue_request(Request(url, priority=priority))
priorities = list()
priorities = []
while self.scheduler.has_pending_requests():
priorities.append(self.scheduler.next_request().priority)
@ -167,7 +167,7 @@ class BaseSchedulerOnDiskTester(SchedulerHandler):
self.close_scheduler()
self.create_scheduler()
priorities = list()
priorities = []
while self.scheduler.has_pending_requests():
priorities.append(self.scheduler.next_request().priority)
@ -259,7 +259,7 @@ class DownloaderAwareSchedulerTestMixin:
self.close_scheduler()
self.create_scheduler()
dequeued_slots = list()
dequeued_slots = []
requests = []
downloader = self.mock_crawler.engine.downloader
while self.scheduler.has_pending_requests():

View File

@ -584,39 +584,6 @@ class DeprecationTest(unittest.TestCase):
assert issubclass(CrawlSpider, Spider)
assert isinstance(CrawlSpider(name='foo'), Spider)
def test_make_requests_from_url_deprecated(self):
class MySpider4(Spider):
name = 'spider1'
start_urls = ['http://example.com']
class MySpider5(Spider):
name = 'spider2'
start_urls = ['http://example.com']
def make_requests_from_url(self, url):
return Request(url + "/foo", dont_filter=True)
with warnings.catch_warnings(record=True) as w:
# spider without overridden make_requests_from_url method
# doesn't issue a warning
spider1 = MySpider4()
self.assertEqual(len(list(spider1.start_requests())), 1)
self.assertEqual(len(w), 0)
# spider without overridden make_requests_from_url method
# should issue a warning when called directly
request = spider1.make_requests_from_url("http://www.example.com")
self.assertTrue(isinstance(request, Request))
self.assertEqual(len(w), 1)
# spider with overridden make_requests_from_url issues a warning,
# but the method still works
spider2 = MySpider5()
requests = list(spider2.start_requests())
self.assertEqual(len(requests), 1)
self.assertEqual(requests[0].url, 'http://example.com/foo')
self.assertEqual(len(w), 2)
class NoParseMethodSpiderTest(unittest.TestCase):

View File

@ -15,7 +15,7 @@ class SpiderMiddlewareTestCase(TestCase):
def setUp(self):
self.request = Request('http://example.com/index.html')
self.response = Response(self.request.url, request=self.request)
self.crawler = get_crawler(Spider)
self.crawler = get_crawler(Spider, {'SPIDER_MIDDLEWARES_BASE': {}})
self.spider = self.crawler._create_spider('foo')
self.mwman = SpiderMiddlewareManager.from_crawler(self.crawler)

View File

@ -176,7 +176,7 @@ class FeedExportConfigTestCase(unittest.TestCase):
"store_empty": True,
"uri_params": (1, 2, 3, 4),
"batch_item_count": 2,
"item_export_kwargs": dict(),
"item_export_kwargs": {},
})
def test_feed_complete_default_values_from_settings_non_empty(self):
@ -199,7 +199,7 @@ class FeedExportConfigTestCase(unittest.TestCase):
"store_empty": True,
"uri_params": None,
"batch_item_count": 2,
"item_export_kwargs": dict(),
"item_export_kwargs": {},
})

View File

@ -27,7 +27,7 @@ class UtilsMiscTestCase(unittest.TestCase):
def test_load_object_exceptions(self):
self.assertRaises(ImportError, load_object, 'nomodule999.mod.function')
self.assertRaises(NameError, load_object, 'scrapy.utils.misc.load_object999')
self.assertRaises(TypeError, load_object, dict())
self.assertRaises(TypeError, load_object, {})
def test_walk_modules(self):
mods = walk_modules('tests.test_utils_misc.test_walk_modules')