mirror of https://github.com/scrapy/scrapy.git
Merge branch 'master' of https://github.com/AKSHAYSHARMAJS/scrapy into integrate-mime
This commit is contained in:
commit
b2181d76d5
|
|
@ -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
|
||||
|
|
|
|||
14
docs/faq.rst
14
docs/faq.rst
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
1
pylintrc
1
pylintrc
|
|
@ -105,6 +105,7 @@ disable=abstract-method,
|
|||
unnecessary-lambda,
|
||||
unnecessary-pass,
|
||||
unreachable,
|
||||
unspecified-encoding,
|
||||
unsubscriptable-object,
|
||||
unused-argument,
|
||||
unused-import,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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/Lempel–Ziv–Markov_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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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'/'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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ɵ​rd\r\n',
|
||||
encoding='windows-1251',
|
||||
|
|
|
|||
|
|
@ -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_'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class KeywordArgumentsSpider(MockServerSpider):
|
|||
},
|
||||
}
|
||||
|
||||
checks = list()
|
||||
checks = []
|
||||
|
||||
def start_requests(self):
|
||||
data = {'key': 'value', 'number': 123}
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {},
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Reference in New Issue