mirror of https://github.com/scrapy/scrapy.git
Merge remote-tracking branch 'origin/master' into asyncio-startrequests-asyncgen
This commit is contained in:
commit
81483a7d98
12
.travis.yml
12
.travis.yml
|
|
@ -11,16 +11,22 @@ matrix:
|
|||
python: 3.8
|
||||
- env: TOXENV=flake8
|
||||
python: 3.8
|
||||
- env: TOXENV=pylint
|
||||
python: 3.8
|
||||
- env: TOXENV=docs
|
||||
python: 3.7 # Keep in sync with .readthedocs.yml
|
||||
|
||||
- env: TOXENV=pypy3
|
||||
- env: TOXENV=pinned
|
||||
python: 3.5.1
|
||||
dist: trusty
|
||||
- env: TOXENV=asyncio
|
||||
python: 3.5.1 # We use additional code to support 3.5.3 and earlier
|
||||
dist: trusty
|
||||
- env: TOXENV=py
|
||||
python: 3.5
|
||||
- env: TOXENV=pinned
|
||||
python: 3.5
|
||||
- env: TOXENV=asyncio
|
||||
python: 3.5.2
|
||||
python: 3.5 # We use specific code to support >= 3.5.4, < 3.6
|
||||
- env: TOXENV=py
|
||||
python: 3.6
|
||||
- env: TOXENV=py
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ including a list of features.
|
|||
Requirements
|
||||
============
|
||||
|
||||
* Python 3.5+
|
||||
* Python 3.5.1+
|
||||
* Works on Linux, Windows, macOS, BSD
|
||||
|
||||
Install
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ Here's an example spider using BeautifulSoup API, with ``lxml`` as the HTML pars
|
|||
What Python versions does Scrapy support?
|
||||
-----------------------------------------
|
||||
|
||||
Scrapy is supported under Python 3.5+
|
||||
Scrapy is supported under Python 3.5.1+
|
||||
under CPython (default Python implementation) and PyPy (starting with PyPy 5.9).
|
||||
Python 3 support was added in Scrapy 1.1.
|
||||
PyPy support was added in Scrapy 1.4, PyPy3 support was added in Scrapy 1.5.
|
||||
|
|
@ -342,14 +342,14 @@ method for this purpose. For example::
|
|||
|
||||
from copy import deepcopy
|
||||
|
||||
from scrapy.item import BaseItem
|
||||
from scrapy.item import Item
|
||||
|
||||
|
||||
class MultiplyItemsMiddleware:
|
||||
|
||||
def process_spider_output(self, response, result, spider):
|
||||
for item in result:
|
||||
if isinstance(item, (BaseItem, dict)):
|
||||
if isinstance(item, (Item, dict)):
|
||||
for _ in range(item['multiply_by']):
|
||||
yield deepcopy(item)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Installation guide
|
|||
Installing Scrapy
|
||||
=================
|
||||
|
||||
Scrapy runs on Python 3.5 or above under CPython (default Python
|
||||
Scrapy runs on Python 3.5.1 or above under CPython (default Python
|
||||
implementation) and PyPy (starting with PyPy 5.9).
|
||||
|
||||
If you're using `Anaconda`_ or `Miniconda`_, you can install the package from
|
||||
|
|
|
|||
|
|
@ -184,6 +184,18 @@ data from it:
|
|||
>>> json.loads(json_data)
|
||||
{'field': 'value'}
|
||||
|
||||
- chompjs_ provides an API to parse JavaScript objects into a :class:`dict`.
|
||||
|
||||
For example, if the JavaScript code contains
|
||||
``var data = {field: "value", secondField: "second value"};``
|
||||
you can extract that data as follows:
|
||||
|
||||
>>> import chompjs
|
||||
>>> javascript = response.css('script::text').get()
|
||||
>>> data = chompjs.parse_js_object(javascript)
|
||||
>>> data
|
||||
{'field': 'value', 'secondField': 'second value'}
|
||||
|
||||
- Otherwise, use js2xml_ to convert the JavaScript code into an XML document
|
||||
that you can parse using :ref:`selectors <topics-selectors>`.
|
||||
|
||||
|
|
@ -241,6 +253,7 @@ along with `scrapy-selenium`_ for seamless integration.
|
|||
|
||||
|
||||
.. _AJAX: https://en.wikipedia.org/wiki/Ajax_%28programming%29
|
||||
.. _chompjs: https://github.com/Nykakin/chompjs
|
||||
.. _CSS: https://en.wikipedia.org/wiki/Cascading_Style_Sheets
|
||||
.. _curl: https://curl.haxx.se/
|
||||
.. _headless browser: https://en.wikipedia.org/wiki/Headless_browser
|
||||
|
|
|
|||
|
|
@ -257,6 +257,4 @@ Field objects
|
|||
Other classes related to Item
|
||||
=============================
|
||||
|
||||
.. autoclass:: BaseItem
|
||||
|
||||
.. autoclass:: ItemMeta
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ this:
|
|||
4. When the files are downloaded, another field (``files``) will be populated
|
||||
with the results. This field will contain a list of dicts with information
|
||||
about the downloaded files, such as the downloaded path, the original
|
||||
scraped url (taken from the ``file_urls`` field) , and the file checksum.
|
||||
scraped url (taken from the ``file_urls`` field), the file checksum and the file status.
|
||||
The files in the list of the ``files`` field will retain the same order of
|
||||
the original ``file_urls`` field. If some file failed downloading, an
|
||||
error will be logged and the file won't be present in the ``files`` field.
|
||||
|
|
@ -470,6 +470,14 @@ See here the methods that you can override in your custom Files Pipeline:
|
|||
|
||||
* ``checksum`` - a `MD5 hash`_ of the image contents
|
||||
|
||||
* ``status`` - the file status indication. It can be one of the following:
|
||||
|
||||
* ``downloaded`` - file was downloaded.
|
||||
* ``uptodate`` - file was not downloaded, as it was downloaded recently,
|
||||
according to the file expiration policy.
|
||||
* ``cached`` - file was already scheduled for download, by another item
|
||||
sharing the same file.
|
||||
|
||||
The list of tuples received by :meth:`~item_completed` is
|
||||
guaranteed to retain the same order of the requests returned from the
|
||||
:meth:`~get_media_requests` method.
|
||||
|
|
@ -479,7 +487,8 @@ See here the methods that you can override in your custom Files Pipeline:
|
|||
[(True,
|
||||
{'checksum': '2b00042f7481c7b056c4b410d28f33cf',
|
||||
'path': 'full/0a79c461a4062ac383dc4fade7bc09f1384a3910.jpg',
|
||||
'url': 'http://www.example.com/files/product1.pdf'}),
|
||||
'url': 'http://www.example.com/files/product1.pdf',
|
||||
'status': 'downloaded'}),
|
||||
(False,
|
||||
Failure(...))]
|
||||
|
||||
|
|
|
|||
|
|
@ -834,11 +834,6 @@ TextResponse objects
|
|||
|
||||
.. automethod:: TextResponse.follow_all
|
||||
|
||||
.. method:: TextResponse.body_as_unicode()
|
||||
|
||||
The same as :attr:`text`, but available as a method. This method is
|
||||
kept for backward compatibility; please prefer ``response.text``.
|
||||
|
||||
|
||||
HtmlResponse objects
|
||||
--------------------
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ engine_started
|
|||
|
||||
Sent when the Scrapy engine has started crawling.
|
||||
|
||||
This signal supports returning deferreds from their handlers.
|
||||
This signal supports returning deferreds from its handlers.
|
||||
|
||||
.. note:: This signal may be fired *after* the :signal:`spider_opened` signal,
|
||||
depending on how the spider was started. So **don't** rely on this signal
|
||||
|
|
@ -127,7 +127,7 @@ engine_stopped
|
|||
Sent when the Scrapy engine is stopped (for example, when a crawling
|
||||
process has finished).
|
||||
|
||||
This signal supports returning deferreds from their handlers.
|
||||
This signal supports returning deferreds from its handlers.
|
||||
|
||||
Item signals
|
||||
------------
|
||||
|
|
@ -149,7 +149,7 @@ item_scraped
|
|||
Sent when an item has been scraped, after it has passed all the
|
||||
:ref:`topics-item-pipeline` stages (without being dropped).
|
||||
|
||||
This signal supports returning deferreds from their handlers.
|
||||
This signal supports returning deferreds from its handlers.
|
||||
|
||||
:param item: the item scraped
|
||||
:type item: dict or :class:`~scrapy.item.Item` object
|
||||
|
|
@ -169,7 +169,7 @@ item_dropped
|
|||
Sent after an item has been dropped from the :ref:`topics-item-pipeline`
|
||||
when some stage raised a :exc:`~scrapy.exceptions.DropItem` exception.
|
||||
|
||||
This signal supports returning deferreds from their handlers.
|
||||
This signal supports returning deferreds from its handlers.
|
||||
|
||||
:param item: the item dropped from the :ref:`topics-item-pipeline`
|
||||
:type item: dict or :class:`~scrapy.item.Item` object
|
||||
|
|
@ -194,7 +194,7 @@ item_error
|
|||
Sent when a :ref:`topics-item-pipeline` generates an error (i.e. raises
|
||||
an exception), except :exc:`~scrapy.exceptions.DropItem` exception.
|
||||
|
||||
This signal supports returning deferreds from their handlers.
|
||||
This signal supports returning deferreds from its handlers.
|
||||
|
||||
:param item: the item dropped from the :ref:`topics-item-pipeline`
|
||||
:type item: dict or :class:`~scrapy.item.Item` object
|
||||
|
|
@ -220,7 +220,7 @@ spider_closed
|
|||
Sent after a spider has been closed. This can be used to release per-spider
|
||||
resources reserved on :signal:`spider_opened`.
|
||||
|
||||
This signal supports returning deferreds from their handlers.
|
||||
This signal supports returning deferreds from its handlers.
|
||||
|
||||
:param spider: the spider which has been closed
|
||||
:type spider: :class:`~scrapy.spiders.Spider` object
|
||||
|
|
@ -244,7 +244,7 @@ spider_opened
|
|||
reserve per-spider resources, but can be used for any task that needs to be
|
||||
performed when a spider is opened.
|
||||
|
||||
This signal supports returning deferreds from their handlers.
|
||||
This signal supports returning deferreds from its handlers.
|
||||
|
||||
:param spider: the spider which has been opened
|
||||
:type spider: :class:`~scrapy.spiders.Spider` object
|
||||
|
|
@ -268,7 +268,7 @@ spider_idle
|
|||
You may raise a :exc:`~scrapy.exceptions.DontCloseSpider` exception to
|
||||
prevent the spider from being closed.
|
||||
|
||||
This signal does not support returning deferreds from their handlers.
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
|
||||
:param spider: the spider which has gone idle
|
||||
:type spider: :class:`~scrapy.spiders.Spider` object
|
||||
|
|
@ -287,7 +287,7 @@ spider_error
|
|||
|
||||
Sent when a spider callback generates an error (i.e. raises an exception).
|
||||
|
||||
This signal does not support returning deferreds from their handlers.
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
|
||||
:param failure: the exception raised
|
||||
:type failure: twisted.python.failure.Failure
|
||||
|
|
@ -310,7 +310,7 @@ request_scheduled
|
|||
Sent when the engine schedules a :class:`~scrapy.http.Request`, to be
|
||||
downloaded later.
|
||||
|
||||
The signal does not support returning deferreds from their handlers.
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
|
||||
:param request: the request that reached the scheduler
|
||||
:type request: :class:`~scrapy.http.Request` object
|
||||
|
|
@ -327,7 +327,7 @@ request_dropped
|
|||
Sent when a :class:`~scrapy.http.Request`, scheduled by the engine to be
|
||||
downloaded later, is rejected by the scheduler.
|
||||
|
||||
The signal does not support returning deferreds from their handlers.
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
|
||||
:param request: the request that reached the scheduler
|
||||
:type request: :class:`~scrapy.http.Request` object
|
||||
|
|
@ -343,7 +343,7 @@ request_reached_downloader
|
|||
|
||||
Sent when a :class:`~scrapy.http.Request` reached downloader.
|
||||
|
||||
The signal does not support returning deferreds from their handlers.
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
|
||||
:param request: the request that reached downloader
|
||||
:type request: :class:`~scrapy.http.Request` object
|
||||
|
|
@ -370,6 +370,29 @@ request_left_downloader
|
|||
:param spider: the spider that yielded the request
|
||||
:type spider: :class:`~scrapy.spiders.Spider` object
|
||||
|
||||
bytes_received
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. signal:: bytes_received
|
||||
.. function:: bytes_received(data, request, spider)
|
||||
|
||||
Sent by the HTTP 1.1 and S3 download handlers when a group of bytes is
|
||||
received for a specific request. This signal might be fired multiple
|
||||
times for the same request, with partial data each time. For instance,
|
||||
a possible scenario for a 25 kb response would be two signals fired
|
||||
with 10 kb of data, and a final one with 5 kb of data.
|
||||
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
|
||||
:param data: the data received by the download handler
|
||||
:type spider: :class:`bytes` object
|
||||
|
||||
:param request: the request that generated the response
|
||||
:type request: :class:`~scrapy.http.Request` object
|
||||
|
||||
:param spider: the spider associated with the response
|
||||
:type spider: :class:`~scrapy.spiders.Spider` object
|
||||
|
||||
Response signals
|
||||
----------------
|
||||
|
||||
|
|
@ -382,7 +405,7 @@ response_received
|
|||
Sent when the engine receives a new :class:`~scrapy.http.Response` from the
|
||||
downloader.
|
||||
|
||||
This signal does not support returning deferreds from their handlers.
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
|
||||
:param response: the response received
|
||||
:type response: :class:`~scrapy.http.Response` object
|
||||
|
|
@ -401,7 +424,7 @@ response_downloaded
|
|||
|
||||
Sent by the downloader right after a ``HTTPResponse`` is downloaded.
|
||||
|
||||
This signal does not support returning deferreds from their handlers.
|
||||
This signal does not support returning deferreds from its handlers.
|
||||
|
||||
:param response: the response downloaded
|
||||
:type response: :class:`~scrapy.http.Response` object
|
||||
|
|
|
|||
|
|
@ -14,50 +14,57 @@ Author: dufferzafar
|
|||
|
||||
import re
|
||||
|
||||
# Used for remembering the file (and its contents)
|
||||
# so we don't have to open the same file again.
|
||||
_filename = None
|
||||
_contents = None
|
||||
|
||||
# A regex that matches standard linkcheck output lines
|
||||
line_re = re.compile(u'(.*)\:\d+\:\s\[(.*)\]\s(?:(.*)\sto\s(.*)|(.*))')
|
||||
def main():
|
||||
|
||||
# Read lines from the linkcheck output file
|
||||
try:
|
||||
with open("build/linkcheck/output.txt") as out:
|
||||
output_lines = out.readlines()
|
||||
except IOError:
|
||||
print("linkcheck output not found; please run linkcheck first.")
|
||||
exit(1)
|
||||
# Used for remembering the file (and its contents)
|
||||
# so we don't have to open the same file again.
|
||||
_filename = None
|
||||
_contents = None
|
||||
|
||||
# For every line, fix the respective file
|
||||
for line in output_lines:
|
||||
match = re.match(line_re, line)
|
||||
# A regex that matches standard linkcheck output lines
|
||||
line_re = re.compile(u'(.*)\:\d+\:\s\[(.*)\]\s(?:(.*)\sto\s(.*)|(.*))')
|
||||
|
||||
if match:
|
||||
newfilename = match.group(1)
|
||||
errortype = match.group(2)
|
||||
# Read lines from the linkcheck output file
|
||||
try:
|
||||
with open("build/linkcheck/output.txt") as out:
|
||||
output_lines = out.readlines()
|
||||
except IOError:
|
||||
print("linkcheck output not found; please run linkcheck first.")
|
||||
exit(1)
|
||||
|
||||
# Broken links can't be fixed and
|
||||
# I am not sure what do with the local ones.
|
||||
if errortype.lower() in ["broken", "local"]:
|
||||
print("Not Fixed: " + line)
|
||||
# For every line, fix the respective file
|
||||
for line in output_lines:
|
||||
match = re.match(line_re, line)
|
||||
|
||||
if match:
|
||||
newfilename = match.group(1)
|
||||
errortype = match.group(2)
|
||||
|
||||
# Broken links can't be fixed and
|
||||
# I am not sure what do with the local ones.
|
||||
if errortype.lower() in ["broken", "local"]:
|
||||
print("Not Fixed: " + line)
|
||||
else:
|
||||
# If this is a new file
|
||||
if newfilename != _filename:
|
||||
|
||||
# Update the previous file
|
||||
if _filename:
|
||||
with open(_filename, "w") as _file:
|
||||
_file.write(_contents)
|
||||
|
||||
_filename = newfilename
|
||||
|
||||
# Read the new file to memory
|
||||
with open(_filename) as _file:
|
||||
_contents = _file.read()
|
||||
|
||||
_contents = _contents.replace(match.group(3), match.group(4))
|
||||
else:
|
||||
# If this is a new file
|
||||
if newfilename != _filename:
|
||||
# We don't understand what the current line means!
|
||||
print("Not Understood: " + line)
|
||||
|
||||
# Update the previous file
|
||||
if _filename:
|
||||
with open(_filename, "w") as _file:
|
||||
_file.write(_contents)
|
||||
|
||||
_filename = newfilename
|
||||
|
||||
# Read the new file to memory
|
||||
with open(_filename) as _file:
|
||||
_contents = _file.read()
|
||||
|
||||
_contents = _contents.replace(match.group(3), match.group(4))
|
||||
else:
|
||||
# We don't understand what the current line means!
|
||||
print("Not Understood: " + line)
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
[MASTER]
|
||||
persistent=no
|
||||
jobs=1 # >1 hides results
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
disable=abstract-method,
|
||||
anomalous-backslash-in-string,
|
||||
arguments-differ,
|
||||
attribute-defined-outside-init,
|
||||
bad-classmethod-argument,
|
||||
bad-continuation,
|
||||
bad-indentation,
|
||||
bad-mcs-classmethod-argument,
|
||||
bad-super-call,
|
||||
bad-whitespace,
|
||||
bare-except,
|
||||
blacklisted-name,
|
||||
broad-except,
|
||||
c-extension-no-member,
|
||||
catching-non-exception,
|
||||
cell-var-from-loop,
|
||||
comparison-with-callable,
|
||||
consider-iterating-dictionary,
|
||||
consider-using-in,
|
||||
consider-using-set-comprehension,
|
||||
consider-using-sys-exit,
|
||||
cyclic-import,
|
||||
dangerous-default-value,
|
||||
deprecated-method,
|
||||
deprecated-module,
|
||||
duplicate-code, # https://github.com/PyCQA/pylint/issues/214
|
||||
eval-used,
|
||||
expression-not-assigned,
|
||||
fixme,
|
||||
function-redefined,
|
||||
global-statement,
|
||||
import-error,
|
||||
import-outside-toplevel,
|
||||
import-self,
|
||||
inconsistent-return-statements,
|
||||
inherit-non-class,
|
||||
invalid-name,
|
||||
invalid-overridden-method,
|
||||
isinstance-second-argument-not-valid-type,
|
||||
keyword-arg-before-vararg,
|
||||
line-too-long,
|
||||
logging-format-interpolation,
|
||||
logging-not-lazy,
|
||||
lost-exception,
|
||||
method-hidden,
|
||||
misplaced-comparison-constant,
|
||||
missing-docstring,
|
||||
missing-final-newline,
|
||||
multiple-imports,
|
||||
multiple-statements,
|
||||
no-else-continue,
|
||||
no-else-raise,
|
||||
no-else-return,
|
||||
no-init,
|
||||
no-member,
|
||||
no-method-argument,
|
||||
no-name-in-module,
|
||||
no-self-argument,
|
||||
no-self-use,
|
||||
no-value-for-parameter,
|
||||
not-an-iterable,
|
||||
not-callable,
|
||||
pointless-statement,
|
||||
pointless-string-statement,
|
||||
protected-access,
|
||||
redefined-argument-from-local,
|
||||
redefined-builtin,
|
||||
redefined-outer-name,
|
||||
reimported,
|
||||
signature-differs,
|
||||
singleton-comparison,
|
||||
super-init-not-called,
|
||||
superfluous-parens,
|
||||
too-few-public-methods,
|
||||
too-many-ancestors,
|
||||
too-many-arguments,
|
||||
too-many-branches,
|
||||
too-many-format-args,
|
||||
too-many-function-args,
|
||||
too-many-instance-attributes,
|
||||
too-many-lines,
|
||||
too-many-locals,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
trailing-newlines,
|
||||
trailing-whitespace,
|
||||
unbalanced-tuple-unpacking,
|
||||
undefined-variable,
|
||||
undefined-loop-variable,
|
||||
unexpected-special-method-signature,
|
||||
ungrouped-imports,
|
||||
unidiomatic-typecheck,
|
||||
unnecessary-comprehension,
|
||||
unnecessary-lambda,
|
||||
unnecessary-pass,
|
||||
unreachable,
|
||||
unsubscriptable-object,
|
||||
unused-argument,
|
||||
unused-import,
|
||||
unused-variable,
|
||||
unused-wildcard-import,
|
||||
used-before-assignment,
|
||||
useless-object-inheritance, # Required for Python 2 support
|
||||
useless-return,
|
||||
useless-super-delegation,
|
||||
wildcard-import,
|
||||
wrong-import-order,
|
||||
wrong-import-position
|
||||
236
pytest.ini
236
pytest.ini
|
|
@ -20,235 +20,25 @@ addopts =
|
|||
twisted = 1
|
||||
markers =
|
||||
only_asyncio: marks tests as only enabled when --reactor=asyncio is passed
|
||||
flake8-max-line-length = 119
|
||||
flake8-ignore =
|
||||
W503
|
||||
# Files that are only meant to provide top-level imports are expected not
|
||||
# to use any of their imports:
|
||||
|
||||
# Exclude files that are meant to provide top-level imports
|
||||
# E402: Module level import not at top of file
|
||||
# F401: Module imported but unused
|
||||
scrapy/core/downloader/handlers/http.py F401
|
||||
scrapy/http/__init__.py F401
|
||||
scrapy/linkextractors/__init__.py E402 F401
|
||||
scrapy/spiders/__init__.py E402 F401
|
||||
|
||||
# Issues pending a review:
|
||||
# extras
|
||||
extras/qps-bench-server.py E501
|
||||
extras/qpsclient.py E501 E501
|
||||
# scrapy/commands
|
||||
scrapy/commands/__init__.py E128 E501
|
||||
scrapy/commands/check.py E501
|
||||
scrapy/commands/crawl.py E501
|
||||
scrapy/commands/edit.py E501
|
||||
scrapy/commands/fetch.py E501 E128
|
||||
scrapy/commands/genspider.py E128 E501
|
||||
scrapy/commands/parse.py E128 E501
|
||||
scrapy/commands/runspider.py E501
|
||||
scrapy/commands/settings.py E128
|
||||
scrapy/commands/shell.py E128 E501
|
||||
scrapy/commands/startproject.py E501 E128
|
||||
scrapy/commands/version.py E501 E128
|
||||
# scrapy/contracts
|
||||
scrapy/contracts/__init__.py E501
|
||||
scrapy/contracts/default.py E128
|
||||
# scrapy/core
|
||||
scrapy/core/engine.py E501 E128
|
||||
scrapy/core/scheduler.py E501
|
||||
scrapy/core/scraper.py E501 E128
|
||||
scrapy/core/spidermw.py E501
|
||||
scrapy/core/downloader/__init__.py E501
|
||||
scrapy/core/downloader/contextfactory.py E501 E128
|
||||
scrapy/core/downloader/middleware.py E501
|
||||
scrapy/core/downloader/tls.py E501
|
||||
scrapy/core/downloader/webclient.py E501 E128
|
||||
scrapy/core/downloader/handlers/__init__.py E501
|
||||
scrapy/core/downloader/handlers/ftp.py E501 E128
|
||||
scrapy/core/downloader/handlers/http10.py E501
|
||||
scrapy/core/downloader/handlers/http11.py E501
|
||||
scrapy/core/downloader/handlers/s3.py E501 E128
|
||||
# scrapy/downloadermiddlewares
|
||||
scrapy/downloadermiddlewares/ajaxcrawl.py E501
|
||||
scrapy/downloadermiddlewares/decompression.py E501
|
||||
scrapy/downloadermiddlewares/defaultheaders.py E501
|
||||
scrapy/downloadermiddlewares/httpcache.py E501
|
||||
scrapy/downloadermiddlewares/httpcompression.py E501 E128
|
||||
scrapy/downloadermiddlewares/httpproxy.py E501
|
||||
scrapy/downloadermiddlewares/redirect.py E501
|
||||
scrapy/downloadermiddlewares/retry.py E501
|
||||
scrapy/downloadermiddlewares/robotstxt.py E501
|
||||
scrapy/downloadermiddlewares/stats.py E501
|
||||
# scrapy/extensions
|
||||
scrapy/extensions/closespider.py E501 E128
|
||||
scrapy/extensions/corestats.py E501
|
||||
scrapy/extensions/feedexport.py E128 E501
|
||||
scrapy/extensions/httpcache.py E128 E501
|
||||
scrapy/extensions/memdebug.py E501
|
||||
scrapy/extensions/spiderstate.py E501
|
||||
scrapy/extensions/telnet.py E501
|
||||
scrapy/extensions/throttle.py E501
|
||||
# scrapy/http
|
||||
scrapy/http/common.py E501
|
||||
scrapy/http/cookies.py E501
|
||||
scrapy/http/request/__init__.py E501
|
||||
scrapy/http/request/form.py E501
|
||||
scrapy/http/request/json_request.py E501
|
||||
scrapy/http/response/__init__.py E501 E128
|
||||
scrapy/http/response/text.py E501 E128
|
||||
# scrapy/linkextractors
|
||||
scrapy/linkextractors/__init__.py E501 E402
|
||||
scrapy/linkextractors/lxmlhtml.py E501
|
||||
# scrapy/loader
|
||||
scrapy/loader/__init__.py E501 E128
|
||||
scrapy/loader/processors.py E501
|
||||
# scrapy/pipelines
|
||||
scrapy/pipelines/__init__.py E501
|
||||
scrapy/pipelines/files.py E116 E501
|
||||
scrapy/pipelines/images.py E501
|
||||
scrapy/pipelines/media.py E501
|
||||
# scrapy/selector
|
||||
scrapy/__init__.py E402
|
||||
scrapy/selector/__init__.py F403
|
||||
scrapy/selector/unified.py E501 E111
|
||||
# scrapy/settings
|
||||
scrapy/settings/__init__.py E501
|
||||
scrapy/settings/default_settings.py E501 E114 E116
|
||||
scrapy/settings/deprecated.py E501
|
||||
# scrapy/spidermiddlewares
|
||||
scrapy/spidermiddlewares/httperror.py E501
|
||||
scrapy/spidermiddlewares/offsite.py E501
|
||||
scrapy/spidermiddlewares/referer.py E501
|
||||
scrapy/spidermiddlewares/urllength.py E501
|
||||
# scrapy/spiders
|
||||
scrapy/spiders/__init__.py E501 E402
|
||||
scrapy/spiders/crawl.py E501
|
||||
scrapy/spiders/feed.py E501
|
||||
scrapy/spiders/sitemap.py E501
|
||||
# scrapy/utils
|
||||
scrapy/utils/asyncio.py E501
|
||||
scrapy/utils/asyncgen.py E501
|
||||
scrapy/utils/benchserver.py E501
|
||||
scrapy/utils/conf.py E402 E501
|
||||
scrapy/utils/datatypes.py E501
|
||||
scrapy/utils/decorators.py E501
|
||||
scrapy/utils/defer.py E501 E128
|
||||
scrapy/utils/deprecate.py E501
|
||||
scrapy/utils/gz.py E501
|
||||
scrapy/spiders/__init__.py E402
|
||||
scrapy/utils/http.py F403
|
||||
scrapy/utils/httpobj.py E501
|
||||
scrapy/utils/iterators.py E501
|
||||
scrapy/utils/log.py E128 E501
|
||||
scrapy/utils/markup.py F403
|
||||
scrapy/utils/middlewares.py E501
|
||||
scrapy/utils/misc.py E501
|
||||
scrapy/utils/multipart.py F403
|
||||
scrapy/utils/project.py E501
|
||||
scrapy/utils/python.py E501
|
||||
scrapy/utils/reactor.py E501
|
||||
scrapy/utils/reqser.py E501
|
||||
scrapy/utils/request.py E501
|
||||
scrapy/utils/response.py E501 E128
|
||||
scrapy/utils/signal.py E501 E128
|
||||
scrapy/utils/sitemap.py E501
|
||||
scrapy/utils/spider.py E501
|
||||
scrapy/utils/ssl.py E501
|
||||
scrapy/utils/test.py E501
|
||||
scrapy/utils/url.py E501 F403 E128 F405
|
||||
# scrapy
|
||||
scrapy/__init__.py E402 E501
|
||||
scrapy/cmdline.py E501
|
||||
scrapy/crawler.py E501
|
||||
scrapy/dupefilters.py E501
|
||||
scrapy/exceptions.py E501
|
||||
scrapy/exporters.py E501
|
||||
scrapy/interfaces.py E501
|
||||
scrapy/item.py E501 E128
|
||||
scrapy/link.py E501
|
||||
scrapy/logformatter.py E501
|
||||
scrapy/mail.py E402 E128 E501
|
||||
scrapy/middleware.py E128 E501
|
||||
scrapy/pqueues.py E501
|
||||
scrapy/resolver.py E501
|
||||
scrapy/responsetypes.py E128 E501
|
||||
scrapy/robotstxt.py E501
|
||||
scrapy/shell.py E501
|
||||
scrapy/signalmanager.py E501
|
||||
scrapy/spiderloader.py F841 E501
|
||||
scrapy/squeues.py E128
|
||||
scrapy/statscollectors.py E501
|
||||
# tests
|
||||
tests/__init__.py E402 E501
|
||||
tests/mockserver.py E501
|
||||
tests/pipelines.py F841
|
||||
tests/spiders.py E501
|
||||
tests/test_closespider.py E501
|
||||
tests/test_command_fetch.py E501
|
||||
tests/test_command_parse.py E501 E128
|
||||
tests/test_command_shell.py E501 E128
|
||||
tests/test_commands.py E128 E501
|
||||
tests/test_contracts.py E501 E128
|
||||
tests/test_crawl.py E501 E741
|
||||
tests/test_crawler.py F841 E501
|
||||
tests/test_dependencies.py F841 E501
|
||||
tests/test_downloader_handlers.py E128 E501
|
||||
tests/test_downloadermiddleware.py E501
|
||||
tests/test_downloadermiddleware_ajaxcrawlable.py E501
|
||||
tests/test_downloadermiddleware_cookies.py E741 E501 E128
|
||||
tests/test_downloadermiddleware_defaultheaders.py E501
|
||||
tests/test_downloadermiddleware_downloadtimeout.py E501
|
||||
tests/test_downloadermiddleware_httpcache.py E501
|
||||
tests/test_downloadermiddleware_httpcompression.py E501
|
||||
tests/test_downloadermiddleware_decompression.py E501
|
||||
tests/test_downloadermiddleware_httpproxy.py E501 E128
|
||||
tests/test_downloadermiddleware_redirect.py E501 E128
|
||||
tests/test_downloadermiddleware_retry.py E501 E128
|
||||
tests/test_downloadermiddleware_robotstxt.py E501
|
||||
tests/test_downloadermiddleware_stats.py E501
|
||||
tests/test_dupefilters.py E501 E741 E128
|
||||
tests/test_engine.py E501 E128
|
||||
tests/test_exporters.py E501 E128
|
||||
tests/test_extension_telnet.py F841
|
||||
tests/test_feedexport.py E501 F841
|
||||
tests/test_http_cookies.py E501
|
||||
tests/test_http_headers.py E501
|
||||
tests/test_http_request.py E402 E501 E128 E128
|
||||
tests/test_http_response.py E501 E128
|
||||
tests/test_item.py E128 F841
|
||||
tests/test_link.py E501
|
||||
tests/test_linkextractors.py E501 E128
|
||||
tests/test_loader.py E501 E741 E128 E117
|
||||
tests/test_logformatter.py E128 E501
|
||||
tests/test_mail.py E128 E501
|
||||
tests/test_middleware.py E501 E128
|
||||
tests/test_pipeline_crawl.py E501 E128
|
||||
tests/test_pipeline_files.py E501
|
||||
tests/test_pipeline_images.py F841 E501
|
||||
tests/test_pipeline_media.py E501 E741 E128
|
||||
tests/test_proxy_connect.py E501 E741
|
||||
tests/test_request_cb_kwargs.py E501
|
||||
tests/test_responsetypes.py E501
|
||||
tests/test_robotstxt_interface.py E501 E501
|
||||
tests/test_scheduler.py E501
|
||||
tests/test_selector.py E501
|
||||
tests/test_spider.py E501
|
||||
tests/test_spidermiddleware.py E501
|
||||
tests/test_spidermiddleware_httperror.py E128 E501 E121
|
||||
tests/test_spidermiddleware_offsite.py E501 E128 E111
|
||||
tests/test_spidermiddleware_output_chain.py E501
|
||||
tests/test_spidermiddleware_referer.py E501 F841 E501 E121
|
||||
tests/test_squeues.py E501 E741
|
||||
tests/test_utils_asyncio.py E501
|
||||
tests/test_utils_asyncgen.py E501
|
||||
tests/test_utils_conf.py E501 E128
|
||||
tests/test_utils_curl.py E501
|
||||
tests/test_utils_datatypes.py E402 E501
|
||||
tests/test_utils_defer.py E501 F841
|
||||
tests/test_utils_deprecate.py F841 E501
|
||||
tests/test_utils_http.py E501 E128
|
||||
tests/test_utils_iterators.py E501 E128
|
||||
tests/test_utils_log.py E741
|
||||
tests/test_utils_python.py E501
|
||||
tests/test_utils_reqser.py E501 E128
|
||||
tests/test_utils_request.py E501 E128
|
||||
tests/test_utils_response.py E501
|
||||
tests/test_utils_signal.py E741 F841
|
||||
tests/test_utils_sitemap.py E128 E501
|
||||
tests/test_utils_url.py E501 E501
|
||||
tests/test_webclient.py E501 E128 E402
|
||||
tests/test_cmdline/__init__.py E501
|
||||
tests/test_settings/__init__.py E501 E128
|
||||
tests/test_spiderloader/__init__.py E128 E501
|
||||
tests/test_utils_misc/__init__.py E501
|
||||
scrapy/utils/url.py F403 F405
|
||||
tests/test_loader.py E741
|
||||
tests/test_webclient.py E402
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ if __name__ == '__main__':
|
|||
try:
|
||||
execute()
|
||||
finally:
|
||||
# Twisted prints errors in DebugInfo.__del__, but PyPy does not run gc.collect()
|
||||
# on exit: http://doc.pypy.org/en/latest/cpython_differences.html?highlight=gc.collect#differences-related-to-garbage-collection-strategies
|
||||
# Twisted prints errors in DebugInfo.__del__, but PyPy does not run gc.collect() on exit:
|
||||
# http://doc.pypy.org/en/latest/cpython_differences.html
|
||||
# ?highlight=gc.collect#differences-related-to-garbage-collection-strategies
|
||||
garbage_collect()
|
||||
|
|
|
|||
|
|
@ -59,17 +59,17 @@ class ScrapyCommand:
|
|||
"""
|
||||
group = OptionGroup(parser, "Global Options")
|
||||
group.add_option("--logfile", metavar="FILE",
|
||||
help="log file. if omitted stderr will be used")
|
||||
help="log file. if omitted stderr will be used")
|
||||
group.add_option("-L", "--loglevel", metavar="LEVEL", default=None,
|
||||
help="log level (default: %s)" % self.settings['LOG_LEVEL'])
|
||||
help="log level (default: %s)" % self.settings['LOG_LEVEL'])
|
||||
group.add_option("--nolog", action="store_true",
|
||||
help="disable logging completely")
|
||||
help="disable logging completely")
|
||||
group.add_option("--profile", metavar="FILE", default=None,
|
||||
help="write python cProfile stats to FILE")
|
||||
help="write python cProfile stats to FILE")
|
||||
group.add_option("--pidfile", metavar="FILE",
|
||||
help="write process ID to FILE")
|
||||
help="write process ID to FILE")
|
||||
group.add_option("-s", "--set", action="append", default=[], metavar="NAME=VALUE",
|
||||
help="set/override setting (may be repeated)")
|
||||
help="set/override setting (may be repeated)")
|
||||
group.add_option("--pdb", action="store_true", help="enable pdb on failure")
|
||||
|
||||
parser.add_option_group(group)
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ class Command(ScrapyCommand):
|
|||
parser.add_option("--spider", dest="spider", help="use this spider")
|
||||
parser.add_option("--headers", dest="headers", action="store_true",
|
||||
help="print response HTTP headers instead of body")
|
||||
parser.add_option("--no-redirect", dest="no_redirect", action="store_true",
|
||||
default=False, help="do not handle HTTP 3xx status codes and print response as-is")
|
||||
parser.add_option("--no-redirect", dest="no_redirect", action="store_true", default=False,
|
||||
help="do not handle HTTP 3xx status codes and print response as-is")
|
||||
|
||||
def _print_headers(self, headers, prefix):
|
||||
for key, values in headers.items():
|
||||
|
|
|
|||
|
|
@ -36,15 +36,15 @@ class Command(ScrapyCommand):
|
|||
def add_options(self, parser):
|
||||
ScrapyCommand.add_options(self, parser)
|
||||
parser.add_option("-l", "--list", dest="list", action="store_true",
|
||||
help="List available templates")
|
||||
help="List available templates")
|
||||
parser.add_option("-e", "--edit", dest="edit", action="store_true",
|
||||
help="Edit spider after creating it")
|
||||
help="Edit spider after creating it")
|
||||
parser.add_option("-d", "--dump", dest="dump", metavar="TEMPLATE",
|
||||
help="Dump template to standard output")
|
||||
help="Dump template to standard output")
|
||||
parser.add_option("-t", "--template", dest="template", default="basic",
|
||||
help="Uses a custom template.")
|
||||
help="Uses a custom template.")
|
||||
parser.add_option("--force", dest="force", action="store_true",
|
||||
help="If the spider already exists, overwrite it with the template")
|
||||
help="If the spider already exists, overwrite it with the template")
|
||||
|
||||
def run(self, args, opts):
|
||||
if opts.list:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from w3lib.url import is_url
|
|||
|
||||
from scrapy.commands import ScrapyCommand
|
||||
from scrapy.http import Request
|
||||
from scrapy.item import BaseItem
|
||||
from scrapy.item import _BaseItem
|
||||
from scrapy.utils import display
|
||||
from scrapy.utils.conf import arglist_to_dict
|
||||
from scrapy.utils.spider import iterate_spider_output, spidercls_for_request
|
||||
|
|
@ -33,29 +33,29 @@ class Command(ScrapyCommand):
|
|||
def add_options(self, parser):
|
||||
ScrapyCommand.add_options(self, parser)
|
||||
parser.add_option("--spider", dest="spider", default=None,
|
||||
help="use this spider without looking for one")
|
||||
help="use this spider without looking for one")
|
||||
parser.add_option("-a", dest="spargs", action="append", default=[], metavar="NAME=VALUE",
|
||||
help="set spider argument (may be repeated)")
|
||||
help="set spider argument (may be repeated)")
|
||||
parser.add_option("--pipelines", action="store_true",
|
||||
help="process items through pipelines")
|
||||
help="process items through pipelines")
|
||||
parser.add_option("--nolinks", dest="nolinks", action="store_true",
|
||||
help="don't show links to follow (extracted requests)")
|
||||
help="don't show links to follow (extracted requests)")
|
||||
parser.add_option("--noitems", dest="noitems", action="store_true",
|
||||
help="don't show scraped items")
|
||||
help="don't show scraped items")
|
||||
parser.add_option("--nocolour", dest="nocolour", action="store_true",
|
||||
help="avoid using pygments to colorize the output")
|
||||
help="avoid using pygments to colorize the output")
|
||||
parser.add_option("-r", "--rules", dest="rules", action="store_true",
|
||||
help="use CrawlSpider rules to discover the callback")
|
||||
help="use CrawlSpider rules to discover the callback")
|
||||
parser.add_option("-c", "--callback", dest="callback",
|
||||
help="use this callback for parsing, instead looking for a callback")
|
||||
help="use this callback for parsing, instead looking for a callback")
|
||||
parser.add_option("-m", "--meta", dest="meta",
|
||||
help="inject extra meta into the Request, it must be a valid raw json string")
|
||||
help="inject extra meta into the Request, it must be a valid raw json string")
|
||||
parser.add_option("--cbkwargs", dest="cbkwargs",
|
||||
help="inject extra callback kwargs into the Request, it must be a valid raw json string")
|
||||
help="inject extra callback kwargs into the Request, it must be a valid raw json string")
|
||||
parser.add_option("-d", "--depth", dest="depth", type="int", default=1,
|
||||
help="maximum depth for parsing requests [default: %default]")
|
||||
help="maximum depth for parsing requests [default: %default]")
|
||||
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
|
||||
help="print each depth level one by one")
|
||||
help="print each depth level one by one")
|
||||
|
||||
@property
|
||||
def max_level(self):
|
||||
|
|
@ -117,7 +117,7 @@ class Command(ScrapyCommand):
|
|||
items, requests = [], []
|
||||
|
||||
for x in iterate_spider_output(callback(response, **cb_kwargs)):
|
||||
if isinstance(x, (BaseItem, dict)):
|
||||
if isinstance(x, (_BaseItem, dict)):
|
||||
items.append(x)
|
||||
elif isinstance(x, Request):
|
||||
requests.append(x)
|
||||
|
|
|
|||
|
|
@ -19,15 +19,15 @@ class Command(ScrapyCommand):
|
|||
def add_options(self, parser):
|
||||
ScrapyCommand.add_options(self, parser)
|
||||
parser.add_option("--get", dest="get", metavar="SETTING",
|
||||
help="print raw setting value")
|
||||
help="print raw setting value")
|
||||
parser.add_option("--getbool", dest="getbool", metavar="SETTING",
|
||||
help="print setting value, interpreted as a boolean")
|
||||
help="print setting value, interpreted as a boolean")
|
||||
parser.add_option("--getint", dest="getint", metavar="SETTING",
|
||||
help="print setting value, interpreted as an integer")
|
||||
help="print setting value, interpreted as an integer")
|
||||
parser.add_option("--getfloat", dest="getfloat", metavar="SETTING",
|
||||
help="print setting value, interpreted as a float")
|
||||
help="print setting value, interpreted as a float")
|
||||
parser.add_option("--getlist", dest="getlist", metavar="SETTING",
|
||||
help="print setting value, interpreted as a list")
|
||||
help="print setting value, interpreted as a list")
|
||||
|
||||
def run(self, args, opts):
|
||||
settings = self.crawler_process.settings
|
||||
|
|
|
|||
|
|
@ -34,11 +34,11 @@ class Command(ScrapyCommand):
|
|||
def add_options(self, parser):
|
||||
ScrapyCommand.add_options(self, parser)
|
||||
parser.add_option("-c", dest="code",
|
||||
help="evaluate the code in the shell, print the result and exit")
|
||||
help="evaluate the code in the shell, print the result and exit")
|
||||
parser.add_option("--spider", dest="spider",
|
||||
help="use this spider")
|
||||
parser.add_option("--no-redirect", dest="no_redirect", action="store_true",
|
||||
default=False, help="do not handle HTTP 3xx status codes and print response as-is")
|
||||
help="use this spider")
|
||||
parser.add_option("--no-redirect", dest="no_redirect", action="store_true", default=False,
|
||||
help="do not handle HTTP 3xx status codes and print response as-is")
|
||||
|
||||
def update_vars(self, vars):
|
||||
"""You can use this function to update the Scrapy objects that will be
|
||||
|
|
|
|||
|
|
@ -102,10 +102,8 @@ class Command(ScrapyCommand):
|
|||
move(join(project_dir, 'module'), join(project_dir, project_name))
|
||||
for paths in TEMPLATES_TO_RENDER:
|
||||
path = join(*paths)
|
||||
tplfile = join(project_dir,
|
||||
string.Template(path).substitute(project_name=project_name))
|
||||
render_templatefile(tplfile, project_name=project_name,
|
||||
ProjectName=string_camelcase(project_name))
|
||||
tplfile = join(project_dir, string.Template(path).substitute(project_name=project_name))
|
||||
render_templatefile(tplfile, project_name=project_name, ProjectName=string_camelcase(project_name))
|
||||
print("New Scrapy project '%s', using template directory '%s', "
|
||||
"created in:" % (project_name, self.templates_dir))
|
||||
print(" %s\n" % abspath(project_dir))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class Command(ScrapyCommand):
|
|||
def add_options(self, parser):
|
||||
ScrapyCommand.add_options(self, parser)
|
||||
parser.add_option("--verbose", "-v", dest="verbose", action="store_true",
|
||||
help="also display twisted/python/platform info (useful for bug reports)")
|
||||
help="also display twisted/python/platform info (useful for bug reports)")
|
||||
|
||||
def run(self, args, opts):
|
||||
if opts.verbose:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import json
|
||||
|
||||
from scrapy.item import BaseItem
|
||||
from scrapy.item import _BaseItem
|
||||
from scrapy.http import Request
|
||||
from scrapy.exceptions import ContractFail
|
||||
|
||||
|
|
@ -51,8 +51,8 @@ class ReturnsContract(Contract):
|
|||
objects = {
|
||||
'request': Request,
|
||||
'requests': Request,
|
||||
'item': (BaseItem, dict),
|
||||
'items': (BaseItem, dict),
|
||||
'item': (_BaseItem, dict),
|
||||
'items': (_BaseItem, dict),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
@ -103,7 +103,7 @@ class ScrapesContract(Contract):
|
|||
|
||||
def post_process(self, output):
|
||||
for x in output:
|
||||
if isinstance(x, (BaseItem, dict)):
|
||||
if isinstance(x, (_BaseItem, dict)):
|
||||
missing = [arg for arg in self.args if arg not in x]
|
||||
if missing:
|
||||
raise ContractFail(
|
||||
|
|
|
|||
|
|
@ -46,11 +46,12 @@ class ScrapyClientContextFactory(BrowserLikePolicyForHTTPS):
|
|||
#
|
||||
# * getattr() for `_ssl_method` attribute for context factories
|
||||
# not calling super(..., self).__init__
|
||||
return CertificateOptions(verify=False,
|
||||
method=getattr(self, 'method',
|
||||
getattr(self, '_ssl_method', None)),
|
||||
fixBrokenPeers=True,
|
||||
acceptableCiphers=self.tls_ciphers)
|
||||
return CertificateOptions(
|
||||
verify=False,
|
||||
method=getattr(self, 'method', getattr(self, '_ssl_method', None)),
|
||||
fixBrokenPeers=True,
|
||||
acceptableCiphers=self.tls_ciphers,
|
||||
)
|
||||
|
||||
# kept for old-style HTTP/1.0 downloader context twisted calls,
|
||||
# e.g. connectSSL()
|
||||
|
|
|
|||
|
|
@ -86,10 +86,9 @@ class FTPDownloadHandler:
|
|||
password = request.meta.get("ftp_password", self.default_password)
|
||||
passive_mode = 1 if bool(request.meta.get("ftp_passive",
|
||||
self.passive_mode)) else 0
|
||||
creator = ClientCreator(reactor, FTPClient, user, password,
|
||||
passive=passive_mode)
|
||||
return creator.connectTCP(parsed_url.hostname, parsed_url.port or 21).addCallback(self.gotClient,
|
||||
request, unquote(parsed_url.path))
|
||||
creator = ClientCreator(reactor, FTPClient, user, password, passive=passive_mode)
|
||||
dfd = creator.connectTCP(parsed_url.hostname, parsed_url.port or 21)
|
||||
return dfd.addCallback(self.gotClient, request, unquote(parsed_url.path))
|
||||
|
||||
def gotClient(self, client, request, filepath):
|
||||
self.client = client
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from twisted.web.http_headers import Headers as TxHeaders
|
|||
from twisted.web.iweb import IBodyProducer, UNKNOWN_LENGTH
|
||||
from zope.interface import implementer
|
||||
|
||||
from scrapy import signals
|
||||
from scrapy.core.downloader.tls import openssl_methods
|
||||
from scrapy.core.downloader.webclient import _parse
|
||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||
|
|
@ -34,6 +35,8 @@ class HTTP11DownloadHandler:
|
|||
lazy = False
|
||||
|
||||
def __init__(self, settings, crawler=None):
|
||||
self._crawler = crawler
|
||||
|
||||
from twisted.internet import reactor
|
||||
self._pool = HTTPConnectionPool(reactor, persistent=True)
|
||||
self._pool.maxPersistentPerHost = settings.getint('CONCURRENT_REQUESTS_PER_DOMAIN')
|
||||
|
|
@ -79,6 +82,7 @@ class HTTP11DownloadHandler:
|
|||
maxsize=getattr(spider, 'download_maxsize', self._default_maxsize),
|
||||
warnsize=getattr(spider, 'download_warnsize', self._default_warnsize),
|
||||
fail_on_dataloss=self._fail_on_dataloss,
|
||||
crawler=self._crawler,
|
||||
)
|
||||
return agent.download_request(request)
|
||||
|
||||
|
|
@ -276,7 +280,7 @@ class ScrapyAgent:
|
|||
_TunnelingAgent = TunnelingAgent
|
||||
|
||||
def __init__(self, contextFactory=None, connectTimeout=10, bindAddress=None, pool=None,
|
||||
maxsize=0, warnsize=0, fail_on_dataloss=True):
|
||||
maxsize=0, warnsize=0, fail_on_dataloss=True, crawler=None):
|
||||
self._contextFactory = contextFactory
|
||||
self._connectTimeout = connectTimeout
|
||||
self._bindAddress = bindAddress
|
||||
|
|
@ -285,6 +289,7 @@ class ScrapyAgent:
|
|||
self._warnsize = warnsize
|
||||
self._fail_on_dataloss = fail_on_dataloss
|
||||
self._txresponse = None
|
||||
self._crawler = crawler
|
||||
|
||||
def _get_agent(self, request, timeout):
|
||||
from twisted.internet import reactor
|
||||
|
|
@ -407,7 +412,15 @@ class ScrapyAgent:
|
|||
|
||||
d = defer.Deferred(_cancel)
|
||||
txresponse.deliverBody(
|
||||
_ResponseReader(d, txresponse, request, maxsize, warnsize, fail_on_dataloss)
|
||||
_ResponseReader(
|
||||
finished=d,
|
||||
txresponse=txresponse,
|
||||
request=request,
|
||||
maxsize=maxsize,
|
||||
warnsize=warnsize,
|
||||
fail_on_dataloss=fail_on_dataloss,
|
||||
crawler=self._crawler,
|
||||
)
|
||||
)
|
||||
|
||||
# save response for timeouts
|
||||
|
|
@ -449,7 +462,7 @@ class _RequestBodyProducer:
|
|||
|
||||
class _ResponseReader(protocol.Protocol):
|
||||
|
||||
def __init__(self, finished, txresponse, request, maxsize, warnsize, fail_on_dataloss):
|
||||
def __init__(self, finished, txresponse, request, maxsize, warnsize, fail_on_dataloss, crawler):
|
||||
self._finished = finished
|
||||
self._txresponse = txresponse
|
||||
self._request = request
|
||||
|
|
@ -462,6 +475,7 @@ class _ResponseReader(protocol.Protocol):
|
|||
self._bytes_received = 0
|
||||
self._certificate = None
|
||||
self._ip_address = None
|
||||
self._crawler = crawler
|
||||
|
||||
def connectionMade(self):
|
||||
if self._certificate is None:
|
||||
|
|
@ -479,6 +493,13 @@ class _ResponseReader(protocol.Protocol):
|
|||
self._bodybuf.write(bodyBytes)
|
||||
self._bytes_received += len(bodyBytes)
|
||||
|
||||
self._crawler.signals.send_catch_log(
|
||||
signal=signals.bytes_received,
|
||||
data=bodyBytes,
|
||||
request=self._request,
|
||||
spider=self._crawler.spider,
|
||||
)
|
||||
|
||||
if self._maxsize and self._bytes_received > self._maxsize:
|
||||
logger.error("Received (%(bytes)s) bytes larger than download "
|
||||
"max size (%(maxsize)s) in request %(request)s.",
|
||||
|
|
|
|||
|
|
@ -228,11 +228,9 @@ class ExecutionEngine:
|
|||
self.slot.nextcall.schedule()
|
||||
|
||||
def schedule(self, request, spider):
|
||||
self.signals.send_catch_log(signal=signals.request_scheduled,
|
||||
request=request, spider=spider)
|
||||
self.signals.send_catch_log(signals.request_scheduled, request=request, spider=spider)
|
||||
if not self.slot.scheduler.enqueue_request(request):
|
||||
self.signals.send_catch_log(signal=signals.request_dropped,
|
||||
request=request, spider=spider)
|
||||
self.signals.send_catch_log(signals.request_dropped, request=request, spider=spider)
|
||||
|
||||
def download(self, request, spider):
|
||||
d = self._download(request, spider)
|
||||
|
|
@ -258,8 +256,8 @@ class ExecutionEngine:
|
|||
logkws = self.logformatter.crawled(request, response, spider)
|
||||
if logkws is not None:
|
||||
logger.log(*logformatter_adapter(logkws), extra={'spider': spider})
|
||||
self.signals.send_catch_log(signal=signals.response_received,
|
||||
response=response, request=request, spider=spider)
|
||||
self.signals.send_catch_log(signals.response_received,
|
||||
response=response, request=request, spider=spider)
|
||||
return response
|
||||
|
||||
def _on_complete(_):
|
||||
|
|
@ -297,8 +295,7 @@ class ExecutionEngine:
|
|||
next loop and this function is guaranteed to be called (at least) once
|
||||
again for this spider.
|
||||
"""
|
||||
res = self.signals.send_catch_log(signal=signals.spider_idle,
|
||||
spider=spider, dont_log=DontCloseSpider)
|
||||
res = self.signals.send_catch_log(signals.spider_idle, spider=spider, dont_log=DontCloseSpider)
|
||||
if any(isinstance(x, Failure) and isinstance(x.value, DontCloseSpider) for _, x in res):
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from scrapy.utils.log import logformatter_adapter, failure_to_exc_info
|
|||
from scrapy.exceptions import CloseSpider, DropItem, IgnoreRequest
|
||||
from scrapy import signals
|
||||
from scrapy.http import Request, Response
|
||||
from scrapy.item import BaseItem
|
||||
from scrapy.item import _BaseItem
|
||||
from scrapy.core.spidermw import SpiderMiddlewareManager
|
||||
|
||||
|
||||
|
|
@ -191,7 +191,7 @@ class Scraper:
|
|||
"""
|
||||
if isinstance(output, Request):
|
||||
self.crawler.engine.crawl(request=output, spider=spider)
|
||||
elif isinstance(output, (BaseItem, dict)):
|
||||
elif isinstance(output, (_BaseItem, dict)):
|
||||
self.slot.itemproc_size += 1
|
||||
dfd = self.itemproc.process_item(output, spider)
|
||||
dfd.addBoth(self._itemproc_finished, output, response, spider)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ def _isiterable(possible_iterator):
|
|||
|
||||
|
||||
def _fname(f):
|
||||
return "%s.%s".format(
|
||||
return "{}.{}".format(
|
||||
f.__self__.__class__.__name__,
|
||||
f.__func__.__name__
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from xml.sax.saxutils import XMLGenerator
|
|||
|
||||
from scrapy.utils.serialize import ScrapyJSONEncoder
|
||||
from scrapy.utils.python import to_bytes, to_unicode, is_listlike
|
||||
from scrapy.item import BaseItem
|
||||
from scrapy.item import _BaseItem
|
||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||
|
||||
|
||||
|
|
@ -250,7 +250,7 @@ class CsvItemExporter(BaseItemExporter):
|
|||
|
||||
class PickleItemExporter(BaseItemExporter):
|
||||
|
||||
def __init__(self, file, protocol=2, **kwargs):
|
||||
def __init__(self, file, protocol=4, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.file = file
|
||||
self.protocol = protocol
|
||||
|
|
@ -312,7 +312,7 @@ class PythonItemExporter(BaseItemExporter):
|
|||
return serializer(value)
|
||||
|
||||
def _serialize_value(self, value):
|
||||
if isinstance(value, BaseItem):
|
||||
if isinstance(value, _BaseItem):
|
||||
return self.export_item(value)
|
||||
if isinstance(value, dict):
|
||||
return dict(self._serialize_dict(value))
|
||||
|
|
|
|||
|
|
@ -46,9 +46,10 @@ class RFC2616Policy:
|
|||
def __init__(self, settings):
|
||||
self.always_store = settings.getbool('HTTPCACHE_ALWAYS_STORE')
|
||||
self.ignore_schemes = settings.getlist('HTTPCACHE_IGNORE_SCHEMES')
|
||||
self.ignore_response_cache_controls = [to_bytes(cc) for cc in
|
||||
settings.getlist('HTTPCACHE_IGNORE_RESPONSE_CACHE_CONTROLS')]
|
||||
self._cc_parsed = WeakKeyDictionary()
|
||||
self.ignore_response_cache_controls = [
|
||||
to_bytes(cc) for cc in settings.getlist('HTTPCACHE_IGNORE_RESPONSE_CACHE_CONTROLS')
|
||||
]
|
||||
|
||||
def _parse_cachecontrol(self, r):
|
||||
if r not in self._cc_parsed:
|
||||
|
|
@ -250,7 +251,7 @@ class DbmCacheStorage:
|
|||
'headers': dict(response.headers),
|
||||
'body': response.body,
|
||||
}
|
||||
self.db['%s_data' % key] = pickle.dumps(data, protocol=2)
|
||||
self.db['%s_data' % key] = pickle.dumps(data, protocol=4)
|
||||
self.db['%s_time' % key] = str(time())
|
||||
|
||||
def _read_data(self, spider, request):
|
||||
|
|
@ -317,7 +318,7 @@ class FilesystemCacheStorage:
|
|||
with self._open(os.path.join(rpath, 'meta'), 'wb') as f:
|
||||
f.write(to_bytes(repr(metadata)))
|
||||
with self._open(os.path.join(rpath, 'pickled_meta'), 'wb') as f:
|
||||
pickle.dump(metadata, f, protocol=2)
|
||||
pickle.dump(metadata, f, protocol=4)
|
||||
with self._open(os.path.join(rpath, 'response_headers'), 'wb') as f:
|
||||
f.write(headers_dict_to_raw(response.headers))
|
||||
with self._open(os.path.join(rpath, 'response_body'), 'wb') as f:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SpiderState:
|
|||
def spider_closed(self, spider):
|
||||
if self.jobdir:
|
||||
with open(self.statefn, 'wb') as f:
|
||||
pickle.dump(spider.state, f, protocol=2)
|
||||
pickle.dump(spider.state, f, protocol=4)
|
||||
|
||||
def spider_opened(self, spider):
|
||||
if self.jobdir and os.path.exists(self.statefn):
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ discovering (through HTTP headers) to base Response class.
|
|||
See documentation in docs/topics/request-response.rst
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from contextlib import suppress
|
||||
from typing import Generator
|
||||
from urllib.parse import urljoin
|
||||
|
|
@ -14,6 +15,7 @@ from w3lib.encoding import (html_body_declared_encoding, html_to_unicode,
|
|||
http_content_type_encoding, resolve_encoding)
|
||||
from w3lib.html import strip_html5_whitespace
|
||||
|
||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||
from scrapy.http import Request
|
||||
from scrapy.http.response import Response
|
||||
from scrapy.utils.python import memoizemethod_noargs, to_unicode
|
||||
|
|
@ -61,6 +63,9 @@ class TextResponse(Response):
|
|||
|
||||
def body_as_unicode(self):
|
||||
"""Return body as unicode"""
|
||||
warnings.warn('Response.body_as_unicode() is deprecated, '
|
||||
'please use Response.text instead.',
|
||||
ScrapyDeprecationWarning, stacklevel=2)
|
||||
return self.text
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -14,28 +14,39 @@ from scrapy.utils.deprecate import ScrapyDeprecationWarning
|
|||
from scrapy.utils.trackref import object_ref
|
||||
|
||||
|
||||
class BaseItem(object_ref):
|
||||
"""Base class for all scraped items.
|
||||
|
||||
In Scrapy, an object is considered an *item* if it is an instance of either
|
||||
:class:`BaseItem` or :class:`dict`. For example, when the output of a
|
||||
spider callback is evaluated, only instances of :class:`BaseItem` or
|
||||
:class:`dict` are passed to :ref:`item pipelines <topics-item-pipeline>`.
|
||||
|
||||
If you need instances of a custom class to be considered items by Scrapy,
|
||||
you must inherit from either :class:`BaseItem` or :class:`dict`.
|
||||
|
||||
Unlike instances of :class:`dict`, instances of :class:`BaseItem` may be
|
||||
:ref:`tracked <topics-leaks-trackrefs>` to debug memory leaks.
|
||||
class _BaseItem(object_ref):
|
||||
"""
|
||||
Temporary class used internally to avoid the deprecation
|
||||
warning raised by isinstance checks using BaseItem.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class _BaseItemMeta(ABCMeta):
|
||||
def __instancecheck__(cls, instance):
|
||||
if cls is BaseItem:
|
||||
warn('scrapy.item.BaseItem is deprecated, please use scrapy.item.Item instead',
|
||||
ScrapyDeprecationWarning, stacklevel=2)
|
||||
return super().__instancecheck__(instance)
|
||||
|
||||
|
||||
class BaseItem(_BaseItem, metaclass=_BaseItemMeta):
|
||||
"""
|
||||
Deprecated, please use :class:`scrapy.item.Item` instead
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if issubclass(cls, BaseItem) and not (issubclass(cls, Item) or issubclass(cls, DictItem)):
|
||||
warn('scrapy.item.BaseItem is deprecated, please use scrapy.item.Item instead',
|
||||
ScrapyDeprecationWarning, stacklevel=2)
|
||||
return super(BaseItem, cls).__new__(cls, *args, **kwargs)
|
||||
|
||||
|
||||
class Field(dict):
|
||||
"""Container of field metadata"""
|
||||
|
||||
|
||||
class ItemMeta(ABCMeta):
|
||||
class ItemMeta(_BaseItemMeta):
|
||||
"""Metaclass_ of :class:`Item` that handles field definitions.
|
||||
|
||||
.. _metaclass: https://realpython.com/python-metaclasses
|
||||
|
|
@ -68,8 +79,7 @@ class DictItem(MutableMapping, BaseItem):
|
|||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if issubclass(cls, DictItem) and not issubclass(cls, Item):
|
||||
warn('scrapy.item.DictItem is deprecated, please use '
|
||||
'scrapy.item.Item instead',
|
||||
warn('scrapy.item.DictItem is deprecated, please use scrapy.item.Item instead',
|
||||
ScrapyDeprecationWarning, stacklevel=2)
|
||||
return super(DictItem, cls).__new__(cls, *args, **kwargs)
|
||||
|
||||
|
|
@ -86,8 +96,7 @@ class DictItem(MutableMapping, BaseItem):
|
|||
if key in self.fields:
|
||||
self._values[key] = value
|
||||
else:
|
||||
raise KeyError("%s does not support field: %s" %
|
||||
(self.__class__.__name__, key))
|
||||
raise KeyError("%s does not support field: %s" % (self.__class__.__name__, key))
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._values[key]
|
||||
|
|
@ -99,8 +108,7 @@ class DictItem(MutableMapping, BaseItem):
|
|||
|
||||
def __setattr__(self, name, value):
|
||||
if not name.startswith('_'):
|
||||
raise AttributeError("Use item[%r] = %r to set field value" %
|
||||
(name, value))
|
||||
raise AttributeError("Use item[%r] = %r to set field value" % (name, value))
|
||||
super(DictItem, self).__setattr__(name, value)
|
||||
|
||||
def __len__(self):
|
||||
|
|
@ -127,4 +135,24 @@ class DictItem(MutableMapping, BaseItem):
|
|||
|
||||
|
||||
class Item(DictItem, metaclass=ItemMeta):
|
||||
pass
|
||||
"""
|
||||
Base class for scraped items.
|
||||
|
||||
In Scrapy, an object is considered an ``item`` if it is an instance of either
|
||||
:class:`Item` or :class:`dict`, or any subclass. For example, when the output of a
|
||||
spider callback is evaluated, only instances of :class:`Item` or
|
||||
:class:`dict` are passed to :ref:`item pipelines <topics-item-pipeline>`.
|
||||
|
||||
If you need instances of a custom class to be considered items by Scrapy,
|
||||
you must inherit from either :class:`Item` or :class:`dict`.
|
||||
|
||||
Items must declare :class:`Field` attributes, which are processed and stored
|
||||
in the ``fields`` attribute. This restricts the set of allowed field names
|
||||
and prevents typos, raising ``KeyError`` when referring to undefined fields.
|
||||
Additionally, fields can be used to define metadata and control the way
|
||||
data is processed internally. Please refer to the :ref:`documentation
|
||||
about fields <topics-items-fields>` for additional information.
|
||||
|
||||
Unlike instances of :class:`dict`, instances of :class:`Item` may be
|
||||
:ref:`tracked <topics-leaks-trackrefs>` to debug memory leaks.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
"""
|
||||
Link extractor based on lxml.html
|
||||
"""
|
||||
import operator
|
||||
from functools import partial
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import lxml.etree as etree
|
||||
|
|
@ -8,10 +10,10 @@ from w3lib.html import strip_html5_whitespace
|
|||
from w3lib.url import canonicalize_url, safe_url_string
|
||||
|
||||
from scrapy.link import Link
|
||||
from scrapy.linkextractors import FilteringLinkExtractor
|
||||
from scrapy.utils.misc import arg_to_iter, rel_has_nofollow
|
||||
from scrapy.utils.python import unique as unique_list
|
||||
from scrapy.utils.response import get_base_url
|
||||
from scrapy.linkextractors import FilteringLinkExtractor
|
||||
|
||||
|
||||
# from lxml/src/lxml/html/__init__.py
|
||||
|
|
@ -27,19 +29,24 @@ def _nons(tag):
|
|||
return tag
|
||||
|
||||
|
||||
def _identity(x):
|
||||
return x
|
||||
|
||||
|
||||
def _canonicalize_link_url(link):
|
||||
return canonicalize_url(link.url, keep_fragments=True)
|
||||
|
||||
|
||||
class LxmlParserLinkExtractor:
|
||||
def __init__(self, tag="a", attr="href", process=None, unique=False,
|
||||
strip=True, canonicalized=False):
|
||||
self.scan_tag = tag if callable(tag) else lambda t: t == tag
|
||||
self.scan_attr = attr if callable(attr) else lambda a: a == attr
|
||||
self.process_attr = process if callable(process) else lambda v: v
|
||||
def __init__(
|
||||
self, tag="a", attr="href", process=None, unique=False, strip=True, canonicalized=False
|
||||
):
|
||||
self.scan_tag = tag if callable(tag) else partial(operator.eq, tag)
|
||||
self.scan_attr = attr if callable(attr) else partial(operator.eq, attr)
|
||||
self.process_attr = process if callable(process) else _identity
|
||||
self.unique = unique
|
||||
self.strip = strip
|
||||
if canonicalized:
|
||||
self.link_key = lambda link: link.url
|
||||
else:
|
||||
self.link_key = lambda link: canonicalize_url(link.url,
|
||||
keep_fragments=True)
|
||||
self.link_key = operator.attrgetter("url") if canonicalized else _canonicalize_link_url
|
||||
|
||||
def _iter_links(self, document):
|
||||
for el in document.iter(etree.Element):
|
||||
|
|
@ -93,25 +100,44 @@ class LxmlParserLinkExtractor:
|
|||
|
||||
class LxmlLinkExtractor(FilteringLinkExtractor):
|
||||
|
||||
def __init__(self, allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(),
|
||||
tags=('a', 'area'), attrs=('href',), canonicalize=False,
|
||||
unique=True, process_value=None, deny_extensions=None, restrict_css=(),
|
||||
strip=True, restrict_text=None):
|
||||
def __init__(
|
||||
self,
|
||||
allow=(),
|
||||
deny=(),
|
||||
allow_domains=(),
|
||||
deny_domains=(),
|
||||
restrict_xpaths=(),
|
||||
tags=('a', 'area'),
|
||||
attrs=('href',),
|
||||
canonicalize=False,
|
||||
unique=True,
|
||||
process_value=None,
|
||||
deny_extensions=None,
|
||||
restrict_css=(),
|
||||
strip=True,
|
||||
restrict_text=None,
|
||||
):
|
||||
tags, attrs = set(arg_to_iter(tags)), set(arg_to_iter(attrs))
|
||||
lx = LxmlParserLinkExtractor(
|
||||
tag=lambda x: x in tags,
|
||||
attr=lambda x: x in attrs,
|
||||
tag=partial(operator.contains, tags),
|
||||
attr=partial(operator.contains, attrs),
|
||||
unique=unique,
|
||||
process=process_value,
|
||||
strip=strip,
|
||||
canonicalized=canonicalize
|
||||
)
|
||||
|
||||
super(LxmlLinkExtractor, self).__init__(lx, allow=allow, deny=deny,
|
||||
allow_domains=allow_domains, deny_domains=deny_domains,
|
||||
restrict_xpaths=restrict_xpaths, restrict_css=restrict_css,
|
||||
canonicalize=canonicalize, deny_extensions=deny_extensions,
|
||||
restrict_text=restrict_text)
|
||||
super(LxmlLinkExtractor, self).__init__(
|
||||
link_extractor=lx,
|
||||
allow=allow,
|
||||
deny=deny,
|
||||
allow_domains=allow_domains,
|
||||
deny_domains=deny_domains,
|
||||
restrict_xpaths=restrict_xpaths,
|
||||
restrict_css=restrict_css,
|
||||
canonicalize=canonicalize,
|
||||
deny_extensions=deny_extensions,
|
||||
restrict_text=restrict_text,
|
||||
)
|
||||
|
||||
def extract_links(self, response):
|
||||
"""Returns a list of :class:`~scrapy.link.Link` objects from the
|
||||
|
|
@ -124,9 +150,11 @@ class LxmlLinkExtractor(FilteringLinkExtractor):
|
|||
"""
|
||||
base_url = get_base_url(response)
|
||||
if self.restrict_xpaths:
|
||||
docs = [subdoc
|
||||
for x in self.restrict_xpaths
|
||||
for subdoc in response.xpath(x)]
|
||||
docs = [
|
||||
subdoc
|
||||
for x in self.restrict_xpaths
|
||||
for subdoc in response.xpath(x)
|
||||
]
|
||||
else:
|
||||
docs = [response.selector]
|
||||
all_links = []
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ def _to_bytes_or_none(text):
|
|||
|
||||
|
||||
class MailSender:
|
||||
def __init__(self, smtphost='localhost', mailfrom='scrapy@localhost',
|
||||
smtpuser=None, smtppass=None, smtpport=25, smtptls=False, smtpssl=False, debug=False):
|
||||
def __init__(
|
||||
self, smtphost='localhost', mailfrom='scrapy@localhost', smtpuser=None,
|
||||
smtppass=None, smtpport=25, smtptls=False, smtpssl=False, debug=False
|
||||
):
|
||||
self.smtphost = smtphost
|
||||
self.smtpport = smtpport
|
||||
self.smtpuser = _to_bytes_or_none(smtpuser)
|
||||
|
|
@ -41,9 +43,15 @@ class MailSender:
|
|||
|
||||
@classmethod
|
||||
def from_settings(cls, settings):
|
||||
return cls(settings['MAIL_HOST'], settings['MAIL_FROM'], settings['MAIL_USER'],
|
||||
settings['MAIL_PASS'], settings.getint('MAIL_PORT'),
|
||||
settings.getbool('MAIL_TLS'), settings.getbool('MAIL_SSL'))
|
||||
return cls(
|
||||
smtphost=settings['MAIL_HOST'],
|
||||
mailfrom=settings['MAIL_FROM'],
|
||||
smtpuser=settings['MAIL_USER'],
|
||||
smtppass=settings['MAIL_PASS'],
|
||||
smtpport=settings.getint('MAIL_PORT'),
|
||||
smtptls=settings.getbool('MAIL_TLS'),
|
||||
smtpssl=settings.getbool('MAIL_SSL'),
|
||||
)
|
||||
|
||||
def send(self, to, subject, body, cc=None, attachs=(), mimetype='text/plain', charset=None, _callback=None):
|
||||
from twisted.internet import reactor
|
||||
|
|
@ -89,9 +97,12 @@ class MailSender:
|
|||
return
|
||||
|
||||
dfd = self._sendmail(rcpts, msg.as_string().encode(charset or 'utf-8'))
|
||||
dfd.addCallbacks(self._sent_ok, self._sent_failed,
|
||||
dfd.addCallbacks(
|
||||
callback=self._sent_ok,
|
||||
errback=self._sent_failed,
|
||||
callbackArgs=[to, cc, subject, len(attachs)],
|
||||
errbackArgs=[to, cc, subject, len(attachs)])
|
||||
errbackArgs=[to, cc, subject, len(attachs)],
|
||||
)
|
||||
reactor.addSystemEventTrigger('before', 'shutdown', lambda: dfd)
|
||||
return dfd
|
||||
|
||||
|
|
@ -115,9 +126,10 @@ class MailSender:
|
|||
from twisted.mail.smtp import ESMTPSenderFactory
|
||||
msg = BytesIO(msg)
|
||||
d = defer.Deferred()
|
||||
factory = ESMTPSenderFactory(self.smtpuser, self.smtppass, self.mailfrom,
|
||||
to_addrs, msg, d, heloFallback=True, requireAuthentication=False,
|
||||
requireTransportSecurity=self.smtptls)
|
||||
factory = ESMTPSenderFactory(
|
||||
self.smtpuser, self.smtppass, self.mailfrom, to_addrs, msg, d,
|
||||
heloFallback=True, requireAuthentication=False, requireTransportSecurity=self.smtptls,
|
||||
)
|
||||
factory.noisy = False
|
||||
|
||||
if self.smtpssl:
|
||||
|
|
|
|||
|
|
@ -83,8 +83,7 @@ class S3FilesStore:
|
|||
AWS_USE_SSL = None
|
||||
AWS_VERIFY = None
|
||||
|
||||
POLICY = 'private' # Overriden from settings.FILES_STORE_S3_ACL in
|
||||
# FilesPipeline.from_settings.
|
||||
POLICY = 'private' # Overriden from settings.FILES_STORE_S3_ACL in FilesPipeline.from_settings
|
||||
HEADERS = {
|
||||
'Cache-Control': 'max-age=172800',
|
||||
}
|
||||
|
|
@ -433,7 +432,7 @@ class FilesPipeline(MediaPipeline):
|
|||
self.inc_stats(info.spider, 'uptodate')
|
||||
|
||||
checksum = result.get('checksum', None)
|
||||
return {'url': request.url, 'path': path, 'checksum': checksum}
|
||||
return {'url': request.url, 'path': path, 'checksum': checksum, 'status': 'uptodate'}
|
||||
|
||||
path = self.file_path(request, info=info)
|
||||
dfd = defer.maybeDeferred(self.store.stat_file, path, info)
|
||||
|
|
@ -510,7 +509,7 @@ class FilesPipeline(MediaPipeline):
|
|||
)
|
||||
raise FileException(str(exc))
|
||||
|
||||
return {'url': request.url, 'path': path, 'checksum': checksum}
|
||||
return {'url': request.url, 'path': path, 'checksum': checksum, 'status': status}
|
||||
|
||||
def inc_stats(self, spider, status):
|
||||
spider.crawler.stats.inc_value('file_count', spider=spider)
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@ class ResponseTypes:
|
|||
|
||||
def from_content_disposition(self, content_disposition):
|
||||
try:
|
||||
filename = to_unicode(content_disposition,
|
||||
encoding='latin-1', errors='replace').split(';')[1].split('=')[1]
|
||||
filename = filename.strip('"\'')
|
||||
filename = to_unicode(
|
||||
content_disposition, encoding='latin-1', errors='replace'
|
||||
).split(';')[1].split('=')[1].strip('"\'')
|
||||
return self.from_filename(filename)
|
||||
except IndexError:
|
||||
return Response
|
||||
|
|
|
|||
|
|
@ -17,10 +17,12 @@ def decode_robotstxt(robotstxt_body, spider, to_native_str_type=False):
|
|||
except UnicodeDecodeError:
|
||||
# If we found garbage or robots.txt in an encoding other than UTF-8, disregard it.
|
||||
# Switch to 'allow all' state.
|
||||
logger.warning("Failure while parsing robots.txt. "
|
||||
"File either contains garbage or is in an encoding other than UTF-8, treating it as an empty file.",
|
||||
exc_info=sys.exc_info(),
|
||||
extra={'spider': spider})
|
||||
logger.warning(
|
||||
"Failure while parsing robots.txt. File either contains garbage or "
|
||||
"is in an encoding other than UTF-8, treating it as an empty file.",
|
||||
exc_info=sys.exc_info(),
|
||||
extra={'spider': spider},
|
||||
)
|
||||
robotstxt_body = ''
|
||||
return robotstxt_body
|
||||
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@ class Selector(_ParselSelector, object_ref):
|
|||
selectorlist_cls = SelectorList
|
||||
|
||||
def __init__(self, response=None, text=None, type=None, root=None, **kwargs):
|
||||
if not(response is None or text is None):
|
||||
raise ValueError('%s.__init__() received both response and text'
|
||||
% self.__class__.__name__)
|
||||
if response is not None and text is not None:
|
||||
raise ValueError('%s.__init__() received both response and text'
|
||||
% self.__class__.__name__)
|
||||
|
||||
st = _st(response, type or self._default_type)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from w3lib.url import any_to_uri
|
|||
from scrapy.crawler import Crawler
|
||||
from scrapy.exceptions import IgnoreRequest
|
||||
from scrapy.http import Request, Response
|
||||
from scrapy.item import BaseItem
|
||||
from scrapy.item import _BaseItem
|
||||
from scrapy.settings import Settings
|
||||
from scrapy.spiders import Spider
|
||||
from scrapy.utils.console import start_python_console
|
||||
|
|
@ -26,8 +26,7 @@ from scrapy.utils.console import DEFAULT_PYTHON_SHELLS
|
|||
|
||||
class Shell:
|
||||
|
||||
relevant_classes = (Crawler, Spider, Request, Response, BaseItem,
|
||||
Settings)
|
||||
relevant_classes = (Crawler, Spider, Request, Response, _BaseItem, Settings)
|
||||
|
||||
def __init__(self, crawler, update_vars=None, code=None):
|
||||
self.crawler = crawler
|
||||
|
|
@ -146,14 +145,13 @@ class Shell:
|
|||
b.append("Useful shortcuts:")
|
||||
if self.inthread:
|
||||
b.append(" fetch(url[, redirect=True]) "
|
||||
"Fetch URL and update local objects "
|
||||
"(by default, redirects are followed)")
|
||||
"Fetch URL and update local objects (by default, redirects are followed)")
|
||||
b.append(" fetch(req) "
|
||||
"Fetch a scrapy.Request and update local objects ")
|
||||
b.append(" shelp() Shell help (print this help)")
|
||||
b.append(" view(response) View response in a browser")
|
||||
|
||||
return "\n".join("[s] %s" % l for l in b)
|
||||
return "\n".join("[s] %s" % line for line in b)
|
||||
|
||||
def _is_relevant(self, value):
|
||||
return isinstance(value, self.relevant_classes)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ request_reached_downloader = object()
|
|||
request_left_downloader = object()
|
||||
response_received = object()
|
||||
response_downloaded = object()
|
||||
bytes_received = object()
|
||||
item_scraped = object()
|
||||
item_dropped = object()
|
||||
item_error = object()
|
||||
|
|
|
|||
|
|
@ -54,8 +54,12 @@ class Rule:
|
|||
self.process_request = _get_method(self.process_request, spider)
|
||||
self.process_request_argcount = len(get_func_args(self.process_request))
|
||||
if self.process_request_argcount == 1:
|
||||
msg = 'Rule.process_request should accept two arguments (request, response), accepting only one is deprecated'
|
||||
warnings.warn(msg, category=ScrapyDeprecationWarning, stacklevel=2)
|
||||
warnings.warn(
|
||||
"Rule.process_request should accept two arguments "
|
||||
"(request, response), accepting only one is deprecated",
|
||||
category=ScrapyDeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
def _process_request(self, request, response):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class XMLFeedSpider(Spider):
|
|||
"""This method is called for the nodes matching the provided tag name
|
||||
(itertag). Receives the response and an Selector for each node.
|
||||
Overriding this method is mandatory. Otherwise, you spider won't work.
|
||||
This method must return either a BaseItem, a Request, or a list
|
||||
This method must return either an item, a request, or a list
|
||||
containing any of them.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -96,5 +96,4 @@ def iterloc(it, alt=False):
|
|||
|
||||
# Also consider alternate URLs (xhtml:link rel="alternate")
|
||||
if alt and 'alternate' in d:
|
||||
for l in d['alternate']:
|
||||
yield l
|
||||
yield from d['alternate']
|
||||
|
|
|
|||
|
|
@ -81,12 +81,11 @@ def _scrapy_non_serialization_queue(queue_class):
|
|||
|
||||
def _pickle_serialize(obj):
|
||||
try:
|
||||
return pickle.dumps(obj, protocol=2)
|
||||
# Python <= 3.4 raises pickle.PicklingError here while
|
||||
# 3.5 <= Python < 3.6 raises AttributeError and
|
||||
# Python >= 3.6 raises TypeError
|
||||
return pickle.dumps(obj, protocol=4)
|
||||
# Both pickle.PicklingError and AttributeError can be raised by pickle.dump(s)
|
||||
# TypeError is raised from parsel.Selector
|
||||
except (pickle.PicklingError, AttributeError, TypeError) as e:
|
||||
raise ValueError(str(e))
|
||||
raise ValueError(str(e)) from e
|
||||
|
||||
|
||||
PickleFifoDiskQueueNonRequest = _serializable_queue(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ def _embed_ipython_shell(namespace={}, banner=''):
|
|||
def _embed_bpython_shell(namespace={}, banner=''):
|
||||
"""Start a bpython shell"""
|
||||
import bpython
|
||||
|
||||
@wraps(_embed_bpython_shell)
|
||||
def wrapper(namespace=namespace, banner=''):
|
||||
bpython.embed(locals_=namespace, banner=banner)
|
||||
|
|
@ -37,6 +38,7 @@ def _embed_bpython_shell(namespace={}, banner=''):
|
|||
def _embed_ptpython_shell(namespace={}, banner=''):
|
||||
"""Start a ptpython shell"""
|
||||
import ptpython.repl
|
||||
|
||||
@wraps(_embed_ptpython_shell)
|
||||
def wrapper(namespace=namespace, banner=''):
|
||||
print(banner)
|
||||
|
|
|
|||
|
|
@ -88,8 +88,11 @@ def process_chain_both(callbacks, errbacks, input, *a, **kw):
|
|||
"""Return a Deferred built by chaining the given callbacks and errbacks"""
|
||||
d = defer.Deferred()
|
||||
for cb, eb in zip(callbacks, errbacks):
|
||||
d.addCallbacks(cb, eb, callbackArgs=a, callbackKeywords=kw,
|
||||
errbackArgs=a, errbackKeywords=kw)
|
||||
d.addCallbacks(
|
||||
callback=cb, errback=eb,
|
||||
callbackArgs=a, callbackKeywords=kw,
|
||||
errbackArgs=a, errbackKeywords=kw,
|
||||
)
|
||||
if isinstance(input, failure.Failure):
|
||||
d.errback(input)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class TopLevelFormatter(logging.Filter):
|
|||
self.loggers = loggers or []
|
||||
|
||||
def filter(self, record):
|
||||
if any(record.name.startswith(l + '.') for l in self.loggers):
|
||||
if any(record.name.startswith(logger + '.') for logger in self.loggers):
|
||||
record.name = record.name.split('.', 1)[0]
|
||||
return True
|
||||
|
||||
|
|
@ -142,10 +142,12 @@ def _get_handler(settings):
|
|||
def log_scrapy_info(settings):
|
||||
logger.info("Scrapy %(version)s started (bot: %(bot)s)",
|
||||
{'version': scrapy.__version__, 'bot': settings['BOT_NAME']})
|
||||
logger.info("Versions: %(versions)s",
|
||||
{'versions': ", ".join("%s %s" % (name, version)
|
||||
for name, version in scrapy_components_versions()
|
||||
if name != "Scrapy")})
|
||||
versions = [
|
||||
"%s %s" % (name, version)
|
||||
for name, version in scrapy_components_versions()
|
||||
if name != "Scrapy"
|
||||
]
|
||||
logger.info("Versions: %(versions)s", {'versions': ", ".join(versions)})
|
||||
from twisted.internet import reactor
|
||||
logger.debug("Using reactor: %s.%s", reactor.__module__, reactor.__class__.__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ from w3lib.html import replace_entities
|
|||
|
||||
from scrapy.utils.datatypes import LocalWeakReferencedCache
|
||||
from scrapy.utils.python import flatten, to_unicode
|
||||
from scrapy.item import BaseItem
|
||||
from scrapy.item import _BaseItem
|
||||
|
||||
|
||||
_ITERABLE_SINGLE_VALUES = dict, BaseItem, str, bytes
|
||||
_ITERABLE_SINGLE_VALUES = dict, _BaseItem, str, bytes
|
||||
|
||||
|
||||
def arg_to_iter(arg):
|
||||
|
|
@ -137,17 +137,26 @@ def create_instance(objcls, settings, crawler, *args, **kwargs):
|
|||
``*args`` and ``**kwargs`` are forwarded to the constructors.
|
||||
|
||||
Raises ``ValueError`` if both ``settings`` and ``crawler`` are ``None``.
|
||||
|
||||
Raises ``TypeError`` if the resulting instance is ``None`` (e.g. if an
|
||||
extension has not been implemented correctly).
|
||||
"""
|
||||
if settings is None:
|
||||
if crawler is None:
|
||||
raise ValueError("Specify at least one of settings and crawler.")
|
||||
settings = crawler.settings
|
||||
if crawler and hasattr(objcls, 'from_crawler'):
|
||||
return objcls.from_crawler(crawler, *args, **kwargs)
|
||||
instance = objcls.from_crawler(crawler, *args, **kwargs)
|
||||
method_name = 'from_crawler'
|
||||
elif hasattr(objcls, 'from_settings'):
|
||||
return objcls.from_settings(settings, *args, **kwargs)
|
||||
instance = objcls.from_settings(settings, *args, **kwargs)
|
||||
method_name = 'from_settings'
|
||||
else:
|
||||
return objcls(*args, **kwargs)
|
||||
instance = objcls(*args, **kwargs)
|
||||
method_name = '__new__'
|
||||
if instance is None:
|
||||
raise TypeError("%s.%s returned None" % (objcls.__qualname__, method_name))
|
||||
return instance
|
||||
|
||||
|
||||
@contextmanager
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ def memoizemethod_noargs(method):
|
|||
weak reference to its object
|
||||
"""
|
||||
cache = weakref.WeakKeyDictionary()
|
||||
|
||||
@wraps(method)
|
||||
def new_method(self, *args, **kwargs):
|
||||
if self not in cache:
|
||||
|
|
|
|||
|
|
@ -19,8 +19,7 @@ def get_base_url(response):
|
|||
"""Return the base url of the given response, joined with the response url"""
|
||||
if response not in _baseurl_cache:
|
||||
text = response.text[0:4096]
|
||||
_baseurl_cache[response] = html.get_base_url(text, response.url,
|
||||
response.encoding)
|
||||
_baseurl_cache[response] = html.get_base_url(text, response.url, response.encoding)
|
||||
return _baseurl_cache[response]
|
||||
|
||||
|
||||
|
|
@ -31,8 +30,8 @@ def get_meta_refresh(response, ignore_tags=('script', 'noscript')):
|
|||
"""Parse the http-equiv refrsh parameter from the given response"""
|
||||
if response not in _metaref_cache:
|
||||
text = response.text[0:4096]
|
||||
_metaref_cache[response] = html.get_meta_refresh(text, response.url,
|
||||
response.encoding, ignore_tags=ignore_tags)
|
||||
_metaref_cache[response] = html.get_meta_refresh(
|
||||
text, response.url, response.encoding, ignore_tags=ignore_tags)
|
||||
return _metaref_cache[response]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import decimal
|
|||
from twisted.internet import defer
|
||||
|
||||
from scrapy.http import Request, Response
|
||||
from scrapy.item import BaseItem
|
||||
from scrapy.item import _BaseItem
|
||||
|
||||
|
||||
class ScrapyJSONEncoder(json.JSONEncoder):
|
||||
|
|
@ -26,7 +26,7 @@ class ScrapyJSONEncoder(json.JSONEncoder):
|
|||
return str(o)
|
||||
elif isinstance(o, defer.Deferred):
|
||||
return str(o)
|
||||
elif isinstance(o, BaseItem):
|
||||
elif isinstance(o, _BaseItem):
|
||||
return dict(o)
|
||||
elif isinstance(o, Request):
|
||||
return "<%s %s %s>" % (type(o).__name__, o.method, o.url)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,7 @@ def send_catch_log(signal=Any, sender=Anonymous, *arguments, **named):
|
|||
responses = []
|
||||
for receiver in liveReceivers(getAllReceivers(sender, signal)):
|
||||
try:
|
||||
response = robustApply(receiver, signal=signal, sender=sender,
|
||||
*arguments, **named)
|
||||
response = robustApply(receiver, signal=signal, sender=sender, *arguments, **named)
|
||||
if isinstance(response, Deferred):
|
||||
logger.error("Cannot return deferreds from signal handler: %(receiver)s",
|
||||
{'receiver': receiver}, extra={'spider': spider})
|
||||
|
|
@ -63,8 +62,7 @@ def send_catch_log_deferred(signal=Any, sender=Anonymous, *arguments, **named):
|
|||
spider = named.get('spider', None)
|
||||
dfds = []
|
||||
for receiver in liveReceivers(getAllReceivers(sender, signal)):
|
||||
d = maybeDeferred_coro(robustApply, receiver, signal=signal, sender=sender,
|
||||
*arguments, **named)
|
||||
d = maybeDeferred_coro(robustApply, receiver, signal=signal, sender=sender, *arguments, **named)
|
||||
d.addErrback(logerror, receiver)
|
||||
d.addBoth(lambda result: (receiver, result))
|
||||
dfds.append(d)
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ def url_is_from_any_domain(url, domains):
|
|||
|
||||
def url_is_from_spider(url, spider):
|
||||
"""Return True if the url belongs to the given spider"""
|
||||
return url_is_from_any_domain(url,
|
||||
[spider.name] + list(getattr(spider, 'allowed_domains', [])))
|
||||
return url_is_from_any_domain(url, [spider.name] + list(getattr(spider, 'allowed_domains', [])))
|
||||
|
||||
|
||||
def url_has_any_extension(url, extensions):
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Some pipelines used for testing
|
|||
class ZeroDivisionErrorPipeline:
|
||||
|
||||
def open_spider(self, spider):
|
||||
a = 1 / 0
|
||||
1 / 0
|
||||
|
||||
def process_item(self, item, spider):
|
||||
return item
|
||||
|
|
|
|||
|
|
@ -142,8 +142,8 @@ ITEM_PIPELINES = {'%s.pipelines.MyPipeline': 1}
|
|||
@defer.inlineCallbacks
|
||||
def test_request_without_meta(self):
|
||||
_, _, stderr = yield self.execute(['--spider', self.spider_name,
|
||||
'-c', 'parse_request_without_meta',
|
||||
'--nolinks',
|
||||
'-c', 'parse_request_without_meta',
|
||||
'--nolinks',
|
||||
self.url('/html')])
|
||||
self.assertIn("DEBUG: It Works!", _textmode(stderr))
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,9 @@ class ShellTest(ProcessTest, SiteTest, unittest.TestCase):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def test_redirect_not_follow_302(self):
|
||||
_, out, _ = yield self.execute(['--no-redirect', self.url('/redirect-no-meta-refresh'), '-c', 'response.status'])
|
||||
_, out, _ = yield self.execute(
|
||||
['--no-redirect', self.url('/redirect-no-meta-refresh'), '-c', 'response.status']
|
||||
)
|
||||
assert out.strip().endswith(b'302')
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
|
@ -101,15 +103,13 @@ class ShellTest(ProcessTest, SiteTest, unittest.TestCase):
|
|||
@defer.inlineCallbacks
|
||||
def test_local_nofile(self):
|
||||
filepath = 'file:///tests/sample_data/test_site/nothinghere.html'
|
||||
errcode, out, err = yield self.execute([filepath, '-c', 'item'],
|
||||
check_code=False)
|
||||
errcode, out, err = yield self.execute([filepath, '-c', 'item'], check_code=False)
|
||||
self.assertEqual(errcode, 1, out or err)
|
||||
self.assertIn(b'No such file or directory', err)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_dns_failures(self):
|
||||
url = 'www.somedomainthatdoesntexi.st'
|
||||
errcode, out, err = yield self.execute([url, '-c', 'item'],
|
||||
check_code=False)
|
||||
errcode, out, err = yield self.execute([url, '-c', 'item'], check_code=False)
|
||||
self.assertEqual(errcode, 1, out or err)
|
||||
self.assertIn(b'DNS lookup failed', err)
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@ class VersionTest(ProcessTest, unittest.TestCase):
|
|||
def test_verbose_output(self):
|
||||
encoding = getattr(sys.stdout, 'encoding') or 'utf-8'
|
||||
_, out, _ = yield self.execute(['-v'])
|
||||
headers = [l.partition(":")[0].strip()
|
||||
for l in out.strip().decode(encoding).splitlines()]
|
||||
headers = [
|
||||
line.partition(":")[0].strip()
|
||||
for line in out.strip().decode(encoding).splitlines()
|
||||
]
|
||||
self.assertEqual(headers, ['Scrapy', 'lxml', 'libxml2',
|
||||
'cssselect', 'parsel', 'w3lib',
|
||||
'Twisted', 'Python', 'pyOpenSSL',
|
||||
|
|
|
|||
|
|
@ -232,7 +232,8 @@ class ContractsManagerTest(unittest.TestCase):
|
|||
# extract contracts correctly
|
||||
contracts = self.conman.extract_contracts(spider.returns_request)
|
||||
self.assertEqual(len(contracts), 2)
|
||||
self.assertEqual(frozenset(type(x) for x in contracts),
|
||||
self.assertEqual(
|
||||
frozenset(type(x) for x in contracts),
|
||||
frozenset([UrlContract, ReturnsContract]))
|
||||
|
||||
# returns request for valid method
|
||||
|
|
|
|||
|
|
@ -104,44 +104,44 @@ class CrawlTestCase(TestCase):
|
|||
@defer.inlineCallbacks
|
||||
def test_retry_503(self):
|
||||
crawler = self.runner.create_crawler(SimpleSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/status?n=503"), mockserver=self.mockserver)
|
||||
self._assert_retried(l)
|
||||
self._assert_retried(log)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_retry_conn_failed(self):
|
||||
crawler = self.runner.create_crawler(SimpleSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl("http://localhost:65432/status?n=503", mockserver=self.mockserver)
|
||||
self._assert_retried(l)
|
||||
self._assert_retried(log)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_retry_dns_error(self):
|
||||
crawler = self.runner.create_crawler(SimpleSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
# try to fetch the homepage of a non-existent domain
|
||||
yield crawler.crawl("http://dns.resolution.invalid./", mockserver=self.mockserver)
|
||||
self._assert_retried(l)
|
||||
self._assert_retried(log)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_start_requests_bug_before_yield(self):
|
||||
with LogCapture('scrapy', level=logging.ERROR) as l:
|
||||
with LogCapture('scrapy', level=logging.ERROR) as log:
|
||||
crawler = self.runner.create_crawler(BrokenStartRequestsSpider)
|
||||
yield crawler.crawl(fail_before_yield=1, mockserver=self.mockserver)
|
||||
|
||||
self.assertEqual(len(l.records), 1)
|
||||
record = l.records[0]
|
||||
self.assertEqual(len(log.records), 1)
|
||||
record = log.records[0]
|
||||
self.assertIsNotNone(record.exc_info)
|
||||
self.assertIs(record.exc_info[0], ZeroDivisionError)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_start_requests_bug_yielding(self):
|
||||
with LogCapture('scrapy', level=logging.ERROR) as l:
|
||||
with LogCapture('scrapy', level=logging.ERROR) as log:
|
||||
crawler = self.runner.create_crawler(BrokenStartRequestsSpider)
|
||||
yield crawler.crawl(fail_yielding=1, mockserver=self.mockserver)
|
||||
|
||||
self.assertEqual(len(l.records), 1)
|
||||
record = l.records[0]
|
||||
self.assertEqual(len(log.records), 1)
|
||||
record = log.records[0]
|
||||
self.assertIsNotNone(record.exc_info)
|
||||
self.assertIs(record.exc_info[0], ZeroDivisionError)
|
||||
|
||||
|
|
@ -187,25 +187,25 @@ foo body
|
|||
with multiples lines
|
||||
'''})
|
||||
crawler = self.runner.create_crawler(SimpleSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/raw?{0}".format(query)), mockserver=self.mockserver)
|
||||
self.assertEqual(str(l).count("Got response 200"), 1)
|
||||
self.assertEqual(str(log).count("Got response 200"), 1)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_retry_conn_lost(self):
|
||||
# connection lost after receiving data
|
||||
crawler = self.runner.create_crawler(SimpleSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/drop?abort=0"), mockserver=self.mockserver)
|
||||
self._assert_retried(l)
|
||||
self._assert_retried(log)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_retry_conn_aborted(self):
|
||||
# connection lost before receiving data
|
||||
crawler = self.runner.create_crawler(SimpleSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/drop?abort=1"), mockserver=self.mockserver)
|
||||
self._assert_retried(l)
|
||||
self._assert_retried(log)
|
||||
|
||||
def _assert_retried(self, log):
|
||||
self.assertEqual(str(log).count("Retrying"), 2)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ class CrawlerLoggingTestCase(unittest.TestCase):
|
|||
class MySpider(scrapy.Spider):
|
||||
name = 'spider'
|
||||
|
||||
crawler = Crawler(MySpider, {})
|
||||
Crawler(MySpider, {})
|
||||
assert get_scrapy_root_handler() is None
|
||||
|
||||
def test_spider_custom_settings_log_level(self):
|
||||
|
|
@ -240,13 +240,13 @@ class CrawlerRunnerHasSpider(unittest.TestCase):
|
|||
|
||||
def test_crawler_runner_asyncio_enabled_true(self):
|
||||
if self.reactor_pytest == 'asyncio':
|
||||
runner = CrawlerRunner(settings={
|
||||
CrawlerRunner(settings={
|
||||
"TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor",
|
||||
})
|
||||
else:
|
||||
msg = r"The installed reactor \(.*?\) does not match the requested one \(.*?\)"
|
||||
with self.assertRaisesRegex(Exception, msg):
|
||||
runner = CrawlerRunner(settings={
|
||||
CrawlerRunner(settings={
|
||||
"TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor",
|
||||
})
|
||||
|
||||
|
|
@ -305,8 +305,10 @@ class CrawlerProcessSubprocess(ScriptRunnerMixin, unittest.TestCase):
|
|||
def test_ipv6_default_name_resolver(self):
|
||||
log = self.run_script('default_name_resolver.py')
|
||||
self.assertIn('Spider closed (finished)', log)
|
||||
self.assertIn("twisted.internet.error.DNSLookupError: DNS lookup failed: no results for hostname lookup: ::1.", log)
|
||||
self.assertIn("'downloader/exception_type_count/twisted.internet.error.DNSLookupError': 1,", log)
|
||||
self.assertIn(
|
||||
"twisted.internet.error.DNSLookupError: DNS lookup failed: no results for hostname lookup: ::1.",
|
||||
log)
|
||||
|
||||
def test_ipv6_alternative_name_resolver(self):
|
||||
log = self.run_script('alternative_name_resolver.py')
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class ScrapyUtilsTest(unittest.TestCase):
|
|||
def test_required_openssl_version(self):
|
||||
try:
|
||||
module = import_module('OpenSSL')
|
||||
except ImportError as ex:
|
||||
except ImportError:
|
||||
raise unittest.SkipTest("OpenSSL is not available")
|
||||
|
||||
if hasattr(module, '__version__'):
|
||||
|
|
|
|||
|
|
@ -493,7 +493,10 @@ class Http11TestCase(HttpTestCase):
|
|||
class Https11TestCase(Http11TestCase):
|
||||
scheme = 'https'
|
||||
|
||||
tls_log_message = 'SSL connection certificate: issuer "/C=IE/O=Scrapy/CN=localhost", subject "/C=IE/O=Scrapy/CN=localhost"'
|
||||
tls_log_message = (
|
||||
'SSL connection certificate: issuer "/C=IE/O=Scrapy/CN=localhost", '
|
||||
'subject "/C=IE/O=Scrapy/CN=localhost"'
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_tls_logging(self):
|
||||
|
|
@ -542,7 +545,10 @@ class Https11InvalidDNSPattern(Https11TestCase):
|
|||
from service_identity.exceptions import CertificateError # noqa: F401
|
||||
except ImportError:
|
||||
raise unittest.SkipTest("cryptography lib is too old")
|
||||
self.tls_log_message = 'SSL connection certificate: issuer "/C=IE/O=Scrapy/CN=127.0.0.1", subject "/C=IE/O=Scrapy/CN=127.0.0.1"'
|
||||
self.tls_log_message = (
|
||||
'SSL connection certificate: issuer "/C=IE/O=Scrapy/CN=127.0.0.1", '
|
||||
'subject "/C=IE/O=Scrapy/CN=127.0.0.1"'
|
||||
)
|
||||
super(Https11InvalidDNSPattern, self).setUp()
|
||||
|
||||
|
||||
|
|
@ -730,6 +736,9 @@ class Http11ProxyTestCase(HttpProxyTestCase):
|
|||
|
||||
class HttpDownloadHandlerMock:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def download_request(self, request, spider):
|
||||
return request
|
||||
|
||||
|
|
@ -853,8 +862,7 @@ class S3TestCase(unittest.TestCase):
|
|||
def test_request_signing4(self):
|
||||
# fetches the access control policy sub-resource for the 'johnsmith' bucket.
|
||||
date = 'Tue, 27 Mar 2007 19:44:46 +0000'
|
||||
req = Request('s3://johnsmith/?acl',
|
||||
method='GET', headers={'Date': date})
|
||||
req = Request('s3://johnsmith/?acl', method='GET', headers={'Date': date})
|
||||
with self._mocked_date(date):
|
||||
httpreq = self.download_request(req, self.spider)
|
||||
self.assertEqual(httpreq.headers['Authorization'],
|
||||
|
|
@ -879,8 +887,9 @@ class S3TestCase(unittest.TestCase):
|
|||
with self._mocked_date(date):
|
||||
httpreq = self.download_request(req, self.spider)
|
||||
# botocore does not override Date with x-amz-date
|
||||
self.assertEqual(httpreq.headers['Authorization'],
|
||||
b'AWS 0PN5J17HBGZHT7JJ3X82:k3nL7gH3+PadhTEVn5Ip83xlYzk=')
|
||||
self.assertEqual(
|
||||
httpreq.headers['Authorization'],
|
||||
b'AWS 0PN5J17HBGZHT7JJ3X82:k3nL7gH3+PadhTEVn5Ip83xlYzk=')
|
||||
|
||||
def test_request_signing6(self):
|
||||
# uploads an object to a CNAME style virtual hosted bucket with metadata.
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class CookiesMiddlewareTest(TestCase):
|
|||
mw = CookiesMiddleware.from_crawler(crawler)
|
||||
with LogCapture('scrapy.downloadermiddlewares.cookies',
|
||||
propagate=False,
|
||||
level=logging.DEBUG) as l:
|
||||
level=logging.DEBUG) as log:
|
||||
req = Request('http://scrapytest.org/')
|
||||
res = Response('http://scrapytest.org/',
|
||||
headers={'Set-Cookie': 'C1=value1; path=/'})
|
||||
|
|
@ -71,7 +71,7 @@ class CookiesMiddlewareTest(TestCase):
|
|||
req2 = Request('http://scrapytest.org/sub1/')
|
||||
mw.process_request(req2, crawler.spider)
|
||||
|
||||
l.check(
|
||||
log.check(
|
||||
('scrapy.downloadermiddlewares.cookies',
|
||||
'DEBUG',
|
||||
'Received cookies from: <200 http://scrapytest.org/>\n'
|
||||
|
|
@ -87,7 +87,7 @@ class CookiesMiddlewareTest(TestCase):
|
|||
mw = CookiesMiddleware.from_crawler(crawler)
|
||||
with LogCapture('scrapy.downloadermiddlewares.cookies',
|
||||
propagate=False,
|
||||
level=logging.DEBUG) as l:
|
||||
level=logging.DEBUG) as log:
|
||||
req = Request('http://scrapytest.org/')
|
||||
res = Response('http://scrapytest.org/',
|
||||
headers={'Set-Cookie': 'C1=value1; path=/'})
|
||||
|
|
@ -95,7 +95,7 @@ class CookiesMiddlewareTest(TestCase):
|
|||
req2 = Request('http://scrapytest.org/sub1/')
|
||||
mw.process_request(req2, crawler.spider)
|
||||
|
||||
l.check()
|
||||
log.check()
|
||||
|
||||
def test_do_not_break_on_non_utf8_header(self):
|
||||
req = Request('http://scrapytest.org/')
|
||||
|
|
|
|||
|
|
@ -124,7 +124,8 @@ class HttpCompressionTest(TestCase):
|
|||
'Content-Encoding': 'gzip',
|
||||
}
|
||||
f = BytesIO()
|
||||
plainbody = b"""<html><head><title>Some page</title><meta http-equiv="Content-Type" content="text/html; charset=gb2312">"""
|
||||
plainbody = (b'<html><head><title>Some page</title>'
|
||||
b'<meta http-equiv="Content-Type" content="text/html; charset=gb2312">')
|
||||
zf = GzipFile(fileobj=f, mode='wb')
|
||||
zf.write(plainbody)
|
||||
zf.close()
|
||||
|
|
@ -142,7 +143,8 @@ class HttpCompressionTest(TestCase):
|
|||
'Content-Encoding': 'gzip',
|
||||
}
|
||||
f = BytesIO()
|
||||
plainbody = b"""<html><head><title>Some page</title><meta http-equiv="Content-Type" content="text/html; charset=gb2312">"""
|
||||
plainbody = (b'<html><head><title>Some page</title>'
|
||||
b'<meta http-equiv="Content-Type" content="text/html; charset=gb2312">')
|
||||
zf = GzipFile(fileobj=f, mode='wb')
|
||||
zf.write(plainbody)
|
||||
zf.close()
|
||||
|
|
@ -158,7 +160,8 @@ class HttpCompressionTest(TestCase):
|
|||
headers = {
|
||||
'Content-Encoding': 'identity',
|
||||
}
|
||||
plainbody = b"""<html><head><title>Some page</title><meta http-equiv="Content-Type" content="text/html; charset=gb2312">"""
|
||||
plainbody = (b'<html><head><title>Some page</title>'
|
||||
b'<meta http-equiv="Content-Type" content="text/html; charset=gb2312">')
|
||||
respcls = responsetypes.from_args(url="http://www.example.com/index", headers=headers, body=plainbody)
|
||||
response = respcls("http://www.example.com/index", headers=headers, body=plainbody)
|
||||
request = Request("http://www.example.com/index")
|
||||
|
|
|
|||
|
|
@ -43,8 +43,11 @@ class TestHttpProxyMiddleware(TestCase):
|
|||
os.environ.pop('file_proxy', None)
|
||||
mw = HttpProxyMiddleware()
|
||||
|
||||
for url, proxy in [('http://e.com', http_proxy),
|
||||
('https://e.com', https_proxy), ('file://tmp/a', None)]:
|
||||
for url, proxy in [
|
||||
('http://e.com', http_proxy),
|
||||
('https://e.com', https_proxy),
|
||||
('file://tmp/a', None),
|
||||
]:
|
||||
req = Request(url)
|
||||
assert mw.process_request(req, spider) is None
|
||||
self.assertEqual(req.url, url)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class RedirectMiddlewareTest(unittest.TestCase):
|
|||
url = 'http://www.example.com/302'
|
||||
url2 = 'http://www.example.com/redirected2'
|
||||
req = Request(url, method='POST', body='test',
|
||||
headers={'Content-Type': 'text/plain', 'Content-length': '4'})
|
||||
headers={'Content-Type': 'text/plain', 'Content-length': '4'})
|
||||
rsp = Response(url, headers={'Location': url2}, status=302)
|
||||
|
||||
req2 = self.mw.process_response(req, rsp, self.spider)
|
||||
|
|
@ -149,7 +149,10 @@ class RedirectMiddlewareTest(unittest.TestCase):
|
|||
self.assertEqual(req2.url, 'http://scrapytest.org/redirected')
|
||||
self.assertEqual(req2.meta['redirect_urls'], ['http://scrapytest.org/first'])
|
||||
self.assertEqual(req3.url, 'http://scrapytest.org/redirected2')
|
||||
self.assertEqual(req3.meta['redirect_urls'], ['http://scrapytest.org/first', 'http://scrapytest.org/redirected'])
|
||||
self.assertEqual(
|
||||
req3.meta['redirect_urls'],
|
||||
['http://scrapytest.org/first', 'http://scrapytest.org/redirected']
|
||||
)
|
||||
|
||||
def test_redirect_reasons(self):
|
||||
req1 = Request('http://scrapytest.org/first')
|
||||
|
|
@ -279,7 +282,10 @@ class MetaRefreshMiddlewareTest(unittest.TestCase):
|
|||
self.assertEqual(req2.url, 'http://scrapytest.org/redirected')
|
||||
self.assertEqual(req2.meta['redirect_urls'], ['http://scrapytest.org/first'])
|
||||
self.assertEqual(req3.url, 'http://scrapytest.org/redirected2')
|
||||
self.assertEqual(req3.meta['redirect_urls'], ['http://scrapytest.org/first', 'http://scrapytest.org/redirected'])
|
||||
self.assertEqual(
|
||||
req3.meta['redirect_urls'],
|
||||
['http://scrapytest.org/first', 'http://scrapytest.org/redirected']
|
||||
)
|
||||
|
||||
def test_redirect_reasons(self):
|
||||
req1 = Request('http://scrapytest.org/first')
|
||||
|
|
|
|||
|
|
@ -81,9 +81,17 @@ class RetryTest(unittest.TestCase):
|
|||
assert self.crawler.stats.get_value('retry/count') == 2
|
||||
|
||||
def test_twistederrors(self):
|
||||
exceptions = [defer.TimeoutError, TCPTimedOutError, TimeoutError,
|
||||
DNSLookupError, ConnectionRefusedError, ConnectionDone,
|
||||
ConnectError, ConnectionLost, ResponseFailed]
|
||||
exceptions = [
|
||||
ConnectError,
|
||||
ConnectionDone,
|
||||
ConnectionLost,
|
||||
ConnectionRefusedError,
|
||||
defer.TimeoutError,
|
||||
DNSLookupError,
|
||||
ResponseFailed,
|
||||
TCPTimedOutError,
|
||||
TimeoutError,
|
||||
]
|
||||
|
||||
for exc in exceptions:
|
||||
req = Request('http://www.scrapytest.org/%s' % exc.__name__)
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ class RFPDupeFilterTest(unittest.TestCase):
|
|||
shutil.rmtree(path)
|
||||
|
||||
def test_log(self):
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
settings = {'DUPEFILTER_DEBUG': False,
|
||||
'DUPEFILTER_CLASS': __name__ + '.FromCrawlerRFPDupeFilter'}
|
||||
crawler = get_crawler(SimpleSpider, settings_dict=settings)
|
||||
|
|
@ -177,15 +177,19 @@ class RFPDupeFilterTest(unittest.TestCase):
|
|||
dupefilter.log(r2, spider)
|
||||
|
||||
assert crawler.stats.get_value('dupefilter/filtered') == 2
|
||||
l.check_present(('scrapy.dupefilters', 'DEBUG',
|
||||
('Filtered duplicate request: <GET http://scrapytest.org/index.html>'
|
||||
' - no more duplicates will be shown'
|
||||
' (see DUPEFILTER_DEBUG to show all duplicates)')))
|
||||
log.check_present(
|
||||
(
|
||||
'scrapy.dupefilters',
|
||||
'DEBUG',
|
||||
'Filtered duplicate request: <GET http://scrapytest.org/index.html> - no more'
|
||||
' duplicates will be shown (see DUPEFILTER_DEBUG to show all duplicates)'
|
||||
)
|
||||
)
|
||||
|
||||
dupefilter.close('finished')
|
||||
|
||||
def test_log_debug(self):
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
settings = {'DUPEFILTER_DEBUG': True,
|
||||
'DUPEFILTER_CLASS': __name__ + '.FromCrawlerRFPDupeFilter'}
|
||||
crawler = get_crawler(SimpleSpider, settings_dict=settings)
|
||||
|
|
@ -203,11 +207,20 @@ class RFPDupeFilterTest(unittest.TestCase):
|
|||
dupefilter.log(r2, spider)
|
||||
|
||||
assert crawler.stats.get_value('dupefilter/filtered') == 2
|
||||
l.check_present(('scrapy.dupefilters', 'DEBUG',
|
||||
('Filtered duplicate request: <GET http://scrapytest.org/index.html>'
|
||||
' (referer: None)')))
|
||||
l.check_present(('scrapy.dupefilters', 'DEBUG',
|
||||
('Filtered duplicate request: <GET http://scrapytest.org/index.html>'
|
||||
' (referer: http://scrapytest.org/INDEX.html)')))
|
||||
log.check_present(
|
||||
(
|
||||
'scrapy.dupefilters',
|
||||
'DEBUG',
|
||||
'Filtered duplicate request: <GET http://scrapytest.org/index.html> (referer: None)'
|
||||
)
|
||||
)
|
||||
log.check_present(
|
||||
(
|
||||
'scrapy.dupefilters',
|
||||
'DEBUG',
|
||||
'Filtered duplicate request: <GET http://scrapytest.org/index.html>'
|
||||
' (referer: http://scrapytest.org/INDEX.html)'
|
||||
)
|
||||
)
|
||||
|
||||
dupefilter.close('finished')
|
||||
|
|
|
|||
|
|
@ -13,23 +13,25 @@ module with the ``runserver`` argument::
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pytest import mark
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.web import server, static, util
|
||||
from twisted.trial import unittest
|
||||
from twisted.web import server, static, util
|
||||
from pydispatch import dispatcher
|
||||
|
||||
from scrapy import signals
|
||||
from scrapy.core.engine import ExecutionEngine
|
||||
from scrapy.utils.test import get_crawler
|
||||
from pydispatch import dispatcher
|
||||
from tests import tests_datadir
|
||||
from scrapy.spiders import Spider
|
||||
from scrapy.http import Request
|
||||
from scrapy.item import Item, Field
|
||||
from scrapy.linkextractors import LinkExtractor
|
||||
from scrapy.http import Request
|
||||
from scrapy.spiders import Spider
|
||||
from scrapy.utils.signal import disconnect_all
|
||||
from scrapy.utils.test import get_crawler
|
||||
|
||||
from tests import tests_datadir, get_testdata
|
||||
|
||||
|
||||
class TestItem(Item):
|
||||
|
|
@ -94,6 +96,8 @@ def start_test_site(debug=False):
|
|||
r = static.File(root_dir)
|
||||
r.putChild(b"redirect", util.Redirect(b"/redirected"))
|
||||
r.putChild(b"redirected", static.Data(b"Redirected here", "text/plain"))
|
||||
numbers = [str(x).encode("utf8") for x in range(2**14)]
|
||||
r.putChild(b"numbers", static.Data(b"".join(numbers), "text/plain"))
|
||||
|
||||
port = reactor.listenTCP(0, server.Site(r), interface="127.0.0.1")
|
||||
if debug:
|
||||
|
|
@ -113,15 +117,20 @@ class CrawlerRun:
|
|||
self.reqreached = []
|
||||
self.itemerror = []
|
||||
self.itemresp = []
|
||||
self.signals_catched = {}
|
||||
self.bytes = defaultdict(lambda: list())
|
||||
self.signals_caught = {}
|
||||
self.spider_class = spider_class
|
||||
|
||||
def run(self):
|
||||
self.port = start_test_site()
|
||||
self.portno = self.port.getHost().port
|
||||
|
||||
start_urls = [self.geturl("/"), self.geturl("/redirect"),
|
||||
self.geturl("/redirect")] # a duplicate
|
||||
start_urls = [
|
||||
self.geturl("/"),
|
||||
self.geturl("/redirect"),
|
||||
self.geturl("/redirect"), # duplicate
|
||||
self.geturl("/numbers"),
|
||||
]
|
||||
|
||||
for name, signal in vars(signals).items():
|
||||
if not name.startswith('_'):
|
||||
|
|
@ -130,6 +139,7 @@ class CrawlerRun:
|
|||
self.crawler = get_crawler(self.spider_class)
|
||||
self.crawler.signals.connect(self.item_scraped, signals.item_scraped)
|
||||
self.crawler.signals.connect(self.item_error, signals.item_error)
|
||||
self.crawler.signals.connect(self.bytes_received, signals.bytes_received)
|
||||
self.crawler.signals.connect(self.request_scheduled, signals.request_scheduled)
|
||||
self.crawler.signals.connect(self.request_dropped, signals.request_dropped)
|
||||
self.crawler.signals.connect(self.request_reached, signals.request_reached_downloader)
|
||||
|
|
@ -161,6 +171,9 @@ class CrawlerRun:
|
|||
def item_scraped(self, item, spider, response):
|
||||
self.itemresp.append((item, response))
|
||||
|
||||
def bytes_received(self, data, request, spider):
|
||||
self.bytes[request].append(data)
|
||||
|
||||
def request_scheduled(self, request, spider):
|
||||
self.reqplug.append((request, spider))
|
||||
|
||||
|
|
@ -178,7 +191,7 @@ class CrawlerRun:
|
|||
signalargs = kwargs.copy()
|
||||
sig = signalargs.pop('signal')
|
||||
signalargs.pop('sender', None)
|
||||
self.signals_catched[sig] = signalargs
|
||||
self.signals_caught[sig] = signalargs
|
||||
|
||||
|
||||
class EngineTest(unittest.TestCase):
|
||||
|
|
@ -189,16 +202,17 @@ class EngineTest(unittest.TestCase):
|
|||
self.run = CrawlerRun(spider)
|
||||
yield self.run.run()
|
||||
self._assert_visited_urls()
|
||||
self._assert_scheduled_requests(urls_to_visit=8)
|
||||
self._assert_scheduled_requests(urls_to_visit=9)
|
||||
self._assert_downloaded_responses()
|
||||
self._assert_scraped_items()
|
||||
self._assert_signals_catched()
|
||||
self._assert_signals_caught()
|
||||
self._assert_bytes_received()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_crawler_dupefilter(self):
|
||||
self.run = CrawlerRun(TestDupeFilterSpider)
|
||||
yield self.run.run()
|
||||
self._assert_scheduled_requests(urls_to_visit=7)
|
||||
self._assert_scheduled_requests(urls_to_visit=8)
|
||||
self._assert_dropped_requests()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
|
@ -242,8 +256,8 @@ class EngineTest(unittest.TestCase):
|
|||
def _assert_visited_urls(self):
|
||||
must_be_visited = ["/", "/redirect", "/redirected",
|
||||
"/item1.html", "/item2.html", "/item999.html"]
|
||||
urls_visited = set([rp[0].url for rp in self.run.respplug])
|
||||
urls_expected = set([self.run.geturl(p) for p in must_be_visited])
|
||||
urls_visited = {rp[0].url for rp in self.run.respplug}
|
||||
urls_expected = {self.run.geturl(p) for p in must_be_visited}
|
||||
assert urls_expected <= urls_visited, "URLs not visited: %s" % list(urls_expected - urls_visited)
|
||||
|
||||
def _assert_scheduled_requests(self, urls_to_visit=None):
|
||||
|
|
@ -251,8 +265,8 @@ class EngineTest(unittest.TestCase):
|
|||
|
||||
paths_expected = ['/item999.html', '/item2.html', '/item1.html']
|
||||
|
||||
urls_requested = set([rq[0].url for rq in self.run.reqplug])
|
||||
urls_expected = set([self.run.geturl(p) for p in paths_expected])
|
||||
urls_requested = {rq[0].url for rq in self.run.reqplug}
|
||||
urls_expected = {self.run.geturl(p) for p in paths_expected}
|
||||
assert urls_expected <= urls_requested
|
||||
scheduled_requests_count = len(self.run.reqplug)
|
||||
dropped_requests_count = len(self.run.reqdropped)
|
||||
|
|
@ -267,8 +281,8 @@ class EngineTest(unittest.TestCase):
|
|||
|
||||
def _assert_downloaded_responses(self):
|
||||
# response tests
|
||||
self.assertEqual(8, len(self.run.respplug))
|
||||
self.assertEqual(8, len(self.run.reqreached))
|
||||
self.assertEqual(9, len(self.run.respplug))
|
||||
self.assertEqual(9, len(self.run.reqreached))
|
||||
|
||||
for response, _ in self.run.respplug:
|
||||
if self.run.getpath(response.url) == '/item999.html':
|
||||
|
|
@ -301,19 +315,61 @@ class EngineTest(unittest.TestCase):
|
|||
self.assertEqual('Item 2 name', item['name'])
|
||||
self.assertEqual('200', item['price'])
|
||||
|
||||
def _assert_signals_catched(self):
|
||||
assert signals.engine_started in self.run.signals_catched
|
||||
assert signals.engine_stopped in self.run.signals_catched
|
||||
assert signals.spider_opened in self.run.signals_catched
|
||||
assert signals.spider_idle in self.run.signals_catched
|
||||
assert signals.spider_closed in self.run.signals_catched
|
||||
def _assert_bytes_received(self):
|
||||
self.assertEqual(9, len(self.run.bytes))
|
||||
for request, data in self.run.bytes.items():
|
||||
joined_data = b"".join(data)
|
||||
if self.run.getpath(request.url) == "/":
|
||||
self.assertEqual(joined_data, get_testdata("test_site", "index.html"))
|
||||
elif self.run.getpath(request.url) == "/item1.html":
|
||||
self.assertEqual(joined_data, get_testdata("test_site", "item1.html"))
|
||||
elif self.run.getpath(request.url) == "/item2.html":
|
||||
self.assertEqual(joined_data, get_testdata("test_site", "item2.html"))
|
||||
elif self.run.getpath(request.url) == "/redirected":
|
||||
self.assertEqual(joined_data, b"Redirected here")
|
||||
elif self.run.getpath(request.url) == '/redirect':
|
||||
self.assertEqual(
|
||||
joined_data,
|
||||
b"\n<html>\n"
|
||||
b" <head>\n"
|
||||
b" <meta http-equiv=\"refresh\" content=\"0;URL=/redirected\">\n"
|
||||
b" </head>\n"
|
||||
b" <body bgcolor=\"#FFFFFF\" text=\"#000000\">\n"
|
||||
b" <a href=\"/redirected\">click here</a>\n"
|
||||
b" </body>\n"
|
||||
b"</html>\n"
|
||||
)
|
||||
elif self.run.getpath(request.url) == "/tem999.html":
|
||||
self.assertEqual(
|
||||
joined_data,
|
||||
b"\n<html>\n"
|
||||
b" <head><title>404 - No Such Resource</title></head>\n"
|
||||
b" <body>\n"
|
||||
b" <h1>No Such Resource</h1>\n"
|
||||
b" <p>File not found.</p>\n"
|
||||
b" </body>\n"
|
||||
b"</html>\n"
|
||||
)
|
||||
elif self.run.getpath(request.url) == "/numbers":
|
||||
# signal was fired multiple times
|
||||
self.assertTrue(len(data) > 1)
|
||||
# bytes were received in order
|
||||
numbers = [str(x).encode("utf8") for x in range(2**14)]
|
||||
self.assertEqual(joined_data, b"".join(numbers))
|
||||
|
||||
def _assert_signals_caught(self):
|
||||
assert signals.engine_started in self.run.signals_caught
|
||||
assert signals.engine_stopped in self.run.signals_caught
|
||||
assert signals.spider_opened in self.run.signals_caught
|
||||
assert signals.spider_idle in self.run.signals_caught
|
||||
assert signals.spider_closed in self.run.signals_caught
|
||||
|
||||
self.assertEqual({'spider': self.run.spider},
|
||||
self.run.signals_catched[signals.spider_opened])
|
||||
self.run.signals_caught[signals.spider_opened])
|
||||
self.assertEqual({'spider': self.run.spider},
|
||||
self.run.signals_catched[signals.spider_idle])
|
||||
self.run.signals_caught[signals.spider_idle])
|
||||
self.assertEqual({'spider': self.run.spider, 'reason': 'finished'},
|
||||
self.run.signals_catched[signals.spider_closed])
|
||||
self.run.signals_caught[signals.spider_closed])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_close_downloader(self):
|
||||
|
|
|
|||
|
|
@ -111,7 +111,10 @@ class PythonItemExporterTest(BaseItemExporterTest):
|
|||
ie = self._get_exporter()
|
||||
exported = ie.export_item(i3)
|
||||
self.assertEqual(type(exported), dict)
|
||||
self.assertEqual(exported, {'age': {'age': {'age': '22', 'name': u'Joseph'}, 'name': u'Maria'}, 'name': 'Jesus'})
|
||||
self.assertEqual(
|
||||
exported,
|
||||
{'age': {'age': {'age': '22', 'name': u'Joseph'}, 'name': u'Maria'}, 'name': 'Jesus'}
|
||||
)
|
||||
self.assertEqual(type(exported['age']), dict)
|
||||
self.assertEqual(type(exported['age']['age']), dict)
|
||||
|
||||
|
|
@ -121,7 +124,10 @@ class PythonItemExporterTest(BaseItemExporterTest):
|
|||
i3 = TestItem(name=u'Jesus', age=[i2])
|
||||
ie = self._get_exporter()
|
||||
exported = ie.export_item(i3)
|
||||
self.assertEqual(exported, {'age': [{'age': [{'age': '22', 'name': u'Joseph'}], 'name': u'Maria'}], 'name': 'Jesus'})
|
||||
self.assertEqual(
|
||||
exported,
|
||||
{'age': [{'age': [{'age': '22', 'name': u'Joseph'}], 'name': u'Maria'}], 'name': 'Jesus'}
|
||||
)
|
||||
self.assertEqual(type(exported['age'][0]), dict)
|
||||
self.assertEqual(type(exported['age'][0]['age'][0]), dict)
|
||||
|
||||
|
|
@ -131,7 +137,10 @@ class PythonItemExporterTest(BaseItemExporterTest):
|
|||
i3 = TestItem(name=u'Jesus', age=[i2])
|
||||
ie = self._get_exporter()
|
||||
exported = ie.export_item(i3)
|
||||
self.assertEqual(exported, {'age': [{'age': [{'age': '22', 'name': u'Joseph'}], 'name': u'Maria'}], 'name': 'Jesus'})
|
||||
self.assertEqual(
|
||||
exported,
|
||||
{'age': [{'age': [{'age': '22', 'name': u'Joseph'}], 'name': u'Maria'}], 'name': 'Jesus'}
|
||||
)
|
||||
self.assertEqual(type(exported['age'][0]), dict)
|
||||
self.assertEqual(type(exported['age'][0]['age'][0]), dict)
|
||||
|
||||
|
|
@ -328,13 +337,19 @@ class XmlItemExporterTest(BaseItemExporterTest):
|
|||
self.assertXmlEquivalent(fp.getvalue(), expected_value)
|
||||
|
||||
def _check_output(self):
|
||||
expected_value = b'<?xml version="1.0" encoding="utf-8"?>\n<items><item><age>22</age><name>John\xc2\xa3</name></item></items>'
|
||||
expected_value = (
|
||||
b'<?xml version="1.0" encoding="utf-8"?>\n'
|
||||
b'<items><item><age>22</age><name>John\xc2\xa3</name></item></items>'
|
||||
)
|
||||
self.assertXmlEquivalent(self.output.getvalue(), expected_value)
|
||||
|
||||
def test_multivalued_fields(self):
|
||||
self.assertExportResult(
|
||||
TestItem(name=[u'John\xa3', u'Doe']),
|
||||
b'<?xml version="1.0" encoding="utf-8"?>\n<items><item><name><value>John\xc2\xa3</value><value>Doe</value></name></item></items>'
|
||||
(
|
||||
b'<?xml version="1.0" encoding="utf-8"?>\n'
|
||||
b'<items><item><name><value>John\xc2\xa3</value><value>Doe</value></name></item></items>'
|
||||
)
|
||||
)
|
||||
|
||||
def test_nested_item(self):
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ class TelnetExtensionTest(unittest.TestCase):
|
|||
def _get_console_and_portal(self, settings=None):
|
||||
crawler = get_crawler(settings_dict=settings)
|
||||
console = TelnetConsole(crawler)
|
||||
username = console.username
|
||||
password = console.password
|
||||
|
||||
# This function has some side effects we don't need for this test
|
||||
console._get_telnet_vars = lambda: {}
|
||||
|
|
|
|||
|
|
@ -715,13 +715,15 @@ class FeedExportTest(unittest.TestCase):
|
|||
@defer.inlineCallbacks
|
||||
def test_export_encoding(self):
|
||||
items = [dict({'foo': u'Test\xd6'})]
|
||||
header = ['foo']
|
||||
|
||||
formats = {
|
||||
'json': u'[{"foo": "Test\\u00d6"}]'.encode('utf-8'),
|
||||
'jsonlines': u'{"foo": "Test\\u00d6"}\n'.encode('utf-8'),
|
||||
'xml': u'<?xml version="1.0" encoding="utf-8"?>\n<items><item><foo>Test\xd6</foo></item></items>'.encode('utf-8'),
|
||||
'csv': u'foo\r\nTest\xd6\r\n'.encode('utf-8'),
|
||||
'json': '[{"foo": "Test\\u00d6"}]'.encode('utf-8'),
|
||||
'jsonlines': '{"foo": "Test\\u00d6"}\n'.encode('utf-8'),
|
||||
'xml': (
|
||||
'<?xml version="1.0" encoding="utf-8"?>\n'
|
||||
'<items><item><foo>Test\xd6</foo></item></items>'
|
||||
).encode('utf-8'),
|
||||
'csv': 'foo\r\nTest\xd6\r\n'.encode('utf-8'),
|
||||
}
|
||||
|
||||
for fmt, expected in formats.items():
|
||||
|
|
@ -735,10 +737,13 @@ class FeedExportTest(unittest.TestCase):
|
|||
self.assertEqual(expected, data[fmt])
|
||||
|
||||
formats = {
|
||||
'json': u'[{"foo": "Test\xd6"}]'.encode('latin-1'),
|
||||
'jsonlines': u'{"foo": "Test\xd6"}\n'.encode('latin-1'),
|
||||
'xml': u'<?xml version="1.0" encoding="latin-1"?>\n<items><item><foo>Test\xd6</foo></item></items>'.encode('latin-1'),
|
||||
'csv': u'foo\r\nTest\xd6\r\n'.encode('latin-1'),
|
||||
'json': '[{"foo": "Test\xd6"}]'.encode('latin-1'),
|
||||
'jsonlines': '{"foo": "Test\xd6"}\n'.encode('latin-1'),
|
||||
'xml': (
|
||||
'<?xml version="1.0" encoding="latin-1"?>\n'
|
||||
'<items><item><foo>Test\xd6</foo></item></items>'
|
||||
).encode('latin-1'),
|
||||
'csv': 'foo\r\nTest\xd6\r\n'.encode('latin-1'),
|
||||
}
|
||||
|
||||
for fmt, expected in formats.items():
|
||||
|
|
@ -757,9 +762,12 @@ class FeedExportTest(unittest.TestCase):
|
|||
items = [dict({'foo': u'FOO', 'bar': u'BAR'})]
|
||||
|
||||
formats = {
|
||||
'json': u'[\n{"bar": "BAR"}\n]'.encode('utf-8'),
|
||||
'xml': u'<?xml version="1.0" encoding="latin-1"?>\n<items>\n <item>\n <foo>FOO</foo>\n </item>\n</items>'.encode('latin-1'),
|
||||
'csv': u'bar,foo\r\nBAR,FOO\r\n'.encode('utf-8'),
|
||||
'json': '[\n{"bar": "BAR"}\n]'.encode('utf-8'),
|
||||
'xml': (
|
||||
'<?xml version="1.0" encoding="latin-1"?>\n'
|
||||
'<items>\n <item>\n <foo>FOO</foo>\n </item>\n</items>'
|
||||
).encode('latin-1'),
|
||||
'csv': 'bar,foo\r\nBAR,FOO\r\n'.encode('utf-8'),
|
||||
}
|
||||
|
||||
settings = {
|
||||
|
|
|
|||
|
|
@ -415,8 +415,7 @@ class FormRequestTest(RequestTest):
|
|||
# using multiples values for a single key
|
||||
data = {'price': u'\xa3 100', 'colours': ['red', 'blue', 'green']}
|
||||
r3 = self.request_class("http://www.example.com", formdata=data)
|
||||
self.assertQueryEqual(r3.body,
|
||||
b'colours=red&colours=blue&colours=green&price=%C2%A3+100')
|
||||
self.assertQueryEqual(r3.body, b'colours=red&colours=blue&colours=green&price=%C2%A3+100')
|
||||
|
||||
def test_from_response_post(self):
|
||||
response = _buildresponse(
|
||||
|
|
@ -426,8 +425,7 @@ class FormRequestTest(RequestTest):
|
|||
<input type="hidden" name="test2" value="xxx">
|
||||
</form>""",
|
||||
url="http://www.example.com/this/list.html")
|
||||
req = self.request_class.from_response(response,
|
||||
formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
req = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
|
||||
self.assertEqual(req.method, 'POST')
|
||||
self.assertEqual(req.headers[b'Content-type'], b'application/x-www-form-urlencoded')
|
||||
|
|
@ -446,8 +444,7 @@ class FormRequestTest(RequestTest):
|
|||
<input type="hidden" name="test2" value="xxx \xc2\xb5">
|
||||
</form>""",
|
||||
url="http://www.example.com/this/list.html")
|
||||
req = self.request_class.from_response(response,
|
||||
formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
req = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
|
||||
self.assertEqual(req.method, 'POST')
|
||||
self.assertEqual(req.headers[b'Content-type'], b'application/x-www-form-urlencoded')
|
||||
|
|
@ -468,8 +465,7 @@ class FormRequestTest(RequestTest):
|
|||
url="http://www.example.com/this/list.html",
|
||||
encoding='latin1',
|
||||
)
|
||||
req = self.request_class.from_response(response,
|
||||
formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
req = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
|
||||
self.assertEqual(req.method, 'POST')
|
||||
self.assertEqual(req.headers[b'Content-type'], b'application/x-www-form-urlencoded')
|
||||
|
|
@ -488,8 +484,7 @@ class FormRequestTest(RequestTest):
|
|||
<input type="hidden" name="test2" value="xxx µ">
|
||||
</form>""",
|
||||
url="http://www.example.com/this/list.html")
|
||||
req = self.request_class.from_response(response,
|
||||
formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
req = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
|
||||
self.assertEqual(req.method, 'POST')
|
||||
self.assertEqual(req.headers[b'Content-type'], b'application/x-www-form-urlencoded')
|
||||
|
|
@ -549,14 +544,13 @@ class FormRequestTest(RequestTest):
|
|||
<input type="hidden" name="test2" value="xxx">
|
||||
</form>""",
|
||||
url="http://www.example.com/this/list.html")
|
||||
r1 = self.request_class.from_response(response,
|
||||
formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
r1 = self.request_class.from_response(response, formdata={'one': ['two', 'three'], 'six': 'seven'})
|
||||
self.assertEqual(r1.method, 'GET')
|
||||
self.assertEqual(urlparse(r1.url).hostname, "www.example.com")
|
||||
self.assertEqual(urlparse(r1.url).path, "/this/get.php")
|
||||
fs = _qs(r1)
|
||||
self.assertEqual(set(fs[b'test']), set([b'val1', b'val2']))
|
||||
self.assertEqual(set(fs[b'one']), set([b'two', b'three']))
|
||||
self.assertEqual(set(fs[b'test']), {b'val1', b'val2'})
|
||||
self.assertEqual(set(fs[b'one']), {b'two', b'three'})
|
||||
self.assertEqual(fs[b'test2'], [b'xxx'])
|
||||
self.assertEqual(fs[b'six'], [b'seven'])
|
||||
|
||||
|
|
@ -1053,7 +1047,7 @@ class FormRequestTest(RequestTest):
|
|||
</form>''')
|
||||
req = self.request_class.from_response(res)
|
||||
fs = _qs(req)
|
||||
self.assertEqual(set(fs), set([b'h2', b'i2', b'i1', b'i3', b'h1', b'i5', b'i4']))
|
||||
self.assertEqual(set(fs), {b'h2', b'i2', b'i1', b'i3', b'h1', b'i5', b'i4'})
|
||||
|
||||
def test_from_response_xpath(self):
|
||||
response = _buildresponse(
|
||||
|
|
@ -1264,7 +1258,10 @@ class XmlRpcRequestTest(RequestTest):
|
|||
class JsonRequestTest(RequestTest):
|
||||
request_class = JsonRequest
|
||||
default_method = 'GET'
|
||||
default_headers = {b'Content-Type': [b'application/json'], b'Accept': [b'application/json, text/javascript, */*; q=0.01']}
|
||||
default_headers = {
|
||||
b'Content-Type': [b'application/json'],
|
||||
b'Accept': [b'application/json, text/javascript, */*; q=0.01'],
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
warnings.simplefilter("always")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import unittest
|
||||
from warnings import catch_warnings
|
||||
|
||||
from w3lib.encoding import resolve_encoding
|
||||
|
||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||
from scrapy.http import (Request, Response, TextResponse, HtmlResponse,
|
||||
XmlResponse, Headers)
|
||||
from scrapy.selector import Selector
|
||||
|
|
@ -24,7 +26,11 @@ class BaseResponseTest(unittest.TestCase):
|
|||
self.assertTrue(isinstance(self.response_class('http://example.com/', body=b''), self.response_class))
|
||||
self.assertTrue(isinstance(self.response_class('http://example.com/', body=b'body'), self.response_class))
|
||||
# test presence of all optional parameters
|
||||
self.assertTrue(isinstance(self.response_class('http://example.com/', body=b'', headers={}, status=200), self.response_class))
|
||||
self.assertTrue(
|
||||
isinstance(
|
||||
self.response_class('http://example.com/', body=b'', headers={}, status=200), self.response_class
|
||||
)
|
||||
)
|
||||
|
||||
r = self.response_class("http://www.example.com")
|
||||
assert isinstance(r.url, str)
|
||||
|
|
@ -322,13 +328,16 @@ class TextResponseTest(BaseResponseTest):
|
|||
self.assertEqual(resp.url, to_unicode(b'http://www.example.com/price/\xc2\xa3'))
|
||||
resp = self.response_class(url=u"http://www.example.com/price/\xa3", encoding='latin-1')
|
||||
self.assertEqual(resp.url, 'http://www.example.com/price/\xa3')
|
||||
resp = self.response_class(u"http://www.example.com/price/\xa3", headers={"Content-type": ["text/html; charset=utf-8"]})
|
||||
resp = self.response_class(u"http://www.example.com/price/\xa3",
|
||||
headers={"Content-type": ["text/html; charset=utf-8"]})
|
||||
self.assertEqual(resp.url, to_unicode(b'http://www.example.com/price/\xc2\xa3'))
|
||||
resp = self.response_class(u"http://www.example.com/price/\xa3", headers={"Content-type": ["text/html; charset=iso-8859-1"]})
|
||||
resp = self.response_class(u"http://www.example.com/price/\xa3",
|
||||
headers={"Content-type": ["text/html; charset=iso-8859-1"]})
|
||||
self.assertEqual(resp.url, 'http://www.example.com/price/\xa3')
|
||||
|
||||
def test_unicode_body(self):
|
||||
unicode_string = u'\u043a\u0438\u0440\u0438\u043b\u043b\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0442\u0435\u043a\u0441\u0442'
|
||||
unicode_string = ('\u043a\u0438\u0440\u0438\u043b\u043b\u0438\u0447\u0435\u0441\u043a\u0438\u0439 '
|
||||
'\u0442\u0435\u043a\u0441\u0442')
|
||||
self.assertRaises(TypeError, self.response_class, 'http://www.example.com', body=u'unicode body')
|
||||
|
||||
original_string = unicode_string.encode('cp1251')
|
||||
|
|
@ -343,13 +352,18 @@ class TextResponseTest(BaseResponseTest):
|
|||
self.assertEqual(r1.text, unicode_string)
|
||||
|
||||
def test_encoding(self):
|
||||
r1 = self.response_class("http://www.example.com", headers={"Content-type": ["text/html; charset=utf-8"]}, body=b"\xc2\xa3")
|
||||
r1 = self.response_class("http://www.example.com", body=b"\xc2\xa3",
|
||||
headers={"Content-type": ["text/html; charset=utf-8"]})
|
||||
r2 = self.response_class("http://www.example.com", encoding='utf-8', body=u"\xa3")
|
||||
r3 = self.response_class("http://www.example.com", headers={"Content-type": ["text/html; charset=iso-8859-1"]}, body=b"\xa3")
|
||||
r3 = self.response_class("http://www.example.com", body=b"\xa3",
|
||||
headers={"Content-type": ["text/html; charset=iso-8859-1"]})
|
||||
r4 = self.response_class("http://www.example.com", body=b"\xa2\xa3")
|
||||
r5 = self.response_class("http://www.example.com", headers={"Content-type": ["text/html; charset=None"]}, body=b"\xc2\xa3")
|
||||
r6 = self.response_class("http://www.example.com", headers={"Content-type": ["text/html; charset=gb2312"]}, body=b"\xa8D")
|
||||
r7 = self.response_class("http://www.example.com", headers={"Content-type": ["text/html; charset=gbk"]}, body=b"\xa8D")
|
||||
r5 = self.response_class("http://www.example.com", body=b"\xc2\xa3",
|
||||
headers={"Content-type": ["text/html; charset=None"]})
|
||||
r6 = self.response_class("http://www.example.com", body=b"\xa8D",
|
||||
headers={"Content-type": ["text/html; charset=gb2312"]})
|
||||
r7 = self.response_class("http://www.example.com", body=b"\xa8D",
|
||||
headers={"Content-type": ["text/html; charset=gbk"]})
|
||||
|
||||
self.assertEqual(r1._headers_encoding(), "utf-8")
|
||||
self.assertEqual(r2._headers_encoding(), None)
|
||||
|
|
@ -484,8 +498,10 @@ class TextResponseTest(BaseResponseTest):
|
|||
response.xpath("normalize-space(//p[@class=\"content\"])").getall(),
|
||||
)
|
||||
self.assertEqual(
|
||||
response.xpath("//title[count(following::p[@class=$pclass])=$pcount]/text()",
|
||||
pclass="content", pcount=1).getall(),
|
||||
response.xpath(
|
||||
"//title[count(following::p[@class=$pclass])=$pcount]/text()",
|
||||
pclass="content", pcount=1,
|
||||
).getall(),
|
||||
response.xpath("//title[count(following::p[@class=\"content\"])=1]/text()").getall(),
|
||||
)
|
||||
|
||||
|
|
@ -565,12 +581,14 @@ class TextResponseTest(BaseResponseTest):
|
|||
'http://example.com',
|
||||
body=b'''<html><body><a href=" foo\n">click me</a></body></html>'''
|
||||
)
|
||||
self._assert_followed_url(resp.css('a')[0],
|
||||
'http://example.com/foo',
|
||||
response=resp)
|
||||
self._assert_followed_url(resp.css('a::attr(href)')[0],
|
||||
'http://example.com/foo',
|
||||
response=resp)
|
||||
self._assert_followed_url(
|
||||
resp.css('a')[0],
|
||||
'http://example.com/foo',
|
||||
response=resp)
|
||||
self._assert_followed_url(
|
||||
resp.css('a::attr(href)')[0],
|
||||
'http://example.com/foo',
|
||||
response=resp)
|
||||
|
||||
def test_follow_encoding(self):
|
||||
resp1 = self.response_class(
|
||||
|
|
@ -660,6 +678,13 @@ class TextResponseTest(BaseResponseTest):
|
|||
with self.assertRaises(ValueError):
|
||||
response.follow_all(css='a[href*="example.com"]', xpath='//a[contains(@href, "example.com")]')
|
||||
|
||||
def test_body_as_unicode_deprecation_warning(self):
|
||||
with catch_warnings(record=True) as warnings:
|
||||
r1 = self.response_class("http://www.example.com", body=u'Hello', encoding='utf-8')
|
||||
self.assertEqual(r1.body_as_unicode(), u'Hello')
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0].category, ScrapyDeprecationWarning)
|
||||
|
||||
|
||||
class HtmlResponseTest(TextResponseTest):
|
||||
|
||||
|
|
@ -684,7 +709,8 @@ class HtmlResponseTest(TextResponseTest):
|
|||
body = b"""<html><head><title>Some page</title><meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
</head><body>Price: \xa3100</body></html>'
|
||||
"""
|
||||
r3 = self.response_class("http://www.example.com", headers={"Content-type": ["text/html; charset=iso-8859-1"]}, body=body)
|
||||
r3 = self.response_class("http://www.example.com", body=body,
|
||||
headers={"Content-type": ["text/html; charset=iso-8859-1"]})
|
||||
self._assert_response_values(r3, 'iso-8859-1', body)
|
||||
|
||||
# make sure replace() preserves the encoding of the original response
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from unittest import mock
|
|||
from warnings import catch_warnings
|
||||
|
||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||
from scrapy.item import ABCMeta, DictItem, Field, Item, ItemMeta
|
||||
from scrapy.item import ABCMeta, _BaseItem, BaseItem, DictItem, Field, Item, ItemMeta
|
||||
|
||||
|
||||
PY36_PLUS = (sys.version_info.major >= 3) and (sys.version_info.minor >= 6)
|
||||
|
|
@ -131,12 +131,12 @@ class ItemTest(unittest.TestCase):
|
|||
self.assertSortedEqual(list(item.values()), [u'New'])
|
||||
|
||||
def test_metaclass_inheritance(self):
|
||||
class BaseItem(Item):
|
||||
class ParentItem(Item):
|
||||
name = Field()
|
||||
keys = Field()
|
||||
values = Field()
|
||||
|
||||
class TestItem(BaseItem):
|
||||
class TestItem(ParentItem):
|
||||
keys = Field()
|
||||
|
||||
i = TestItem()
|
||||
|
|
@ -162,8 +162,7 @@ class ItemTest(unittest.TestCase):
|
|||
item = D(save='X', load='Y')
|
||||
self.assertEqual(item['save'], 'X')
|
||||
self.assertEqual(item['load'], 'Y')
|
||||
self.assertEqual(D.fields, {'load': {'default': 'A'},
|
||||
'save': {'default': 'A'}})
|
||||
self.assertEqual(D.fields, {'load': {'default': 'A'}, 'save': {'default': 'A'}})
|
||||
|
||||
# D class inverted
|
||||
class E(C, B):
|
||||
|
|
@ -171,8 +170,7 @@ class ItemTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(E(save='X')['save'], 'X')
|
||||
self.assertEqual(E(load='X')['load'], 'X')
|
||||
self.assertEqual(E.fields, {'load': {'default': 'C'},
|
||||
'save': {'default': 'C'}})
|
||||
self.assertEqual(E.fields, {'load': {'default': 'C'}, 'save': {'default': 'C'}})
|
||||
|
||||
def test_metaclass_multiple_inheritance_diamond(self):
|
||||
class A(Item):
|
||||
|
|
@ -193,8 +191,9 @@ class ItemTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(D(save='X')['save'], 'X')
|
||||
self.assertEqual(D(load='X')['load'], 'X')
|
||||
self.assertEqual(D.fields, {'save': {'default': 'C'},
|
||||
'load': {'default': 'D'}, 'update': {'default': 'D'}})
|
||||
self.assertEqual(
|
||||
D.fields,
|
||||
{'save': {'default': 'C'}, 'load': {'default': 'D'}, 'update': {'default': 'D'}})
|
||||
|
||||
# D class inverted
|
||||
class E(C, B):
|
||||
|
|
@ -202,8 +201,9 @@ class ItemTest(unittest.TestCase):
|
|||
|
||||
self.assertEqual(E(save='X')['save'], 'X')
|
||||
self.assertEqual(E(load='X')['load'], 'X')
|
||||
self.assertEqual(E.fields, {'save': {'default': 'C'},
|
||||
'load': {'default': 'E'}, 'update': {'default': 'C'}})
|
||||
self.assertEqual(
|
||||
E.fields,
|
||||
{'save': {'default': 'C'}, 'load': {'default': 'E'}, 'update': {'default': 'C'}})
|
||||
|
||||
def test_metaclass_multiple_inheritance_without_metaclass(self):
|
||||
class A(Item):
|
||||
|
|
@ -223,8 +223,7 @@ class ItemTest(unittest.TestCase):
|
|||
|
||||
self.assertRaises(KeyError, D, not_allowed='value')
|
||||
self.assertEqual(D(save='X')['save'], 'X')
|
||||
self.assertEqual(D.fields, {'save': {'default': 'A'},
|
||||
'load': {'default': 'A'}})
|
||||
self.assertEqual(D.fields, {'save': {'default': 'A'}, 'load': {'default': 'A'}})
|
||||
|
||||
# D class inverted
|
||||
class E(C, B):
|
||||
|
|
@ -232,8 +231,7 @@ class ItemTest(unittest.TestCase):
|
|||
|
||||
self.assertRaises(KeyError, E, not_allowed='value')
|
||||
self.assertEqual(E(save='X')['save'], 'X')
|
||||
self.assertEqual(E.fields, {'save': {'default': 'A'},
|
||||
'load': {'default': 'A'}})
|
||||
self.assertEqual(E.fields, {'save': {'default': 'A'}, 'load': {'default': 'A'}})
|
||||
|
||||
def test_to_dict(self):
|
||||
class TestItem(Item):
|
||||
|
|
@ -264,12 +262,12 @@ class ItemTest(unittest.TestCase):
|
|||
"""Make sure the DictItem deprecation warning is not issued for
|
||||
Item"""
|
||||
with catch_warnings(record=True) as warnings:
|
||||
item = Item()
|
||||
Item()
|
||||
self.assertEqual(len(warnings), 0)
|
||||
|
||||
class SubclassedItem(Item):
|
||||
pass
|
||||
subclassed_item = SubclassedItem()
|
||||
SubclassedItem()
|
||||
self.assertEqual(len(warnings), 0)
|
||||
|
||||
|
||||
|
|
@ -321,16 +319,88 @@ class DictItemTest(unittest.TestCase):
|
|||
|
||||
def test_deprecation_warning(self):
|
||||
with catch_warnings(record=True) as warnings:
|
||||
dict_item = DictItem()
|
||||
DictItem()
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0].category, ScrapyDeprecationWarning)
|
||||
with catch_warnings(record=True) as warnings:
|
||||
class SubclassedDictItem(DictItem):
|
||||
pass
|
||||
subclassed_dict_item = SubclassedDictItem()
|
||||
SubclassedDictItem()
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0].category, ScrapyDeprecationWarning)
|
||||
|
||||
|
||||
class BaseItemTest(unittest.TestCase):
|
||||
|
||||
def test_isinstance_check(self):
|
||||
|
||||
class SubclassedBaseItem(BaseItem):
|
||||
pass
|
||||
|
||||
class SubclassedItem(Item):
|
||||
pass
|
||||
|
||||
self.assertTrue(isinstance(BaseItem(), BaseItem))
|
||||
self.assertTrue(isinstance(SubclassedBaseItem(), BaseItem))
|
||||
self.assertTrue(isinstance(Item(), BaseItem))
|
||||
self.assertTrue(isinstance(SubclassedItem(), BaseItem))
|
||||
|
||||
# make sure internal checks using private _BaseItem class succeed
|
||||
self.assertTrue(isinstance(BaseItem(), _BaseItem))
|
||||
self.assertTrue(isinstance(SubclassedBaseItem(), _BaseItem))
|
||||
self.assertTrue(isinstance(Item(), _BaseItem))
|
||||
self.assertTrue(isinstance(SubclassedItem(), _BaseItem))
|
||||
|
||||
def test_deprecation_warning(self):
|
||||
"""
|
||||
Make sure deprecation warnings are logged whenever BaseItem is used,
|
||||
either instantiated or in an isinstance check
|
||||
"""
|
||||
with catch_warnings(record=True) as warnings:
|
||||
BaseItem()
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0].category, ScrapyDeprecationWarning)
|
||||
|
||||
with catch_warnings(record=True) as warnings:
|
||||
|
||||
class SubclassedBaseItem(BaseItem):
|
||||
pass
|
||||
|
||||
SubclassedBaseItem()
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0].category, ScrapyDeprecationWarning)
|
||||
|
||||
with catch_warnings(record=True) as warnings:
|
||||
self.assertFalse(isinstance("foo", BaseItem))
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0].category, ScrapyDeprecationWarning)
|
||||
|
||||
with catch_warnings(record=True) as warnings:
|
||||
self.assertTrue(isinstance(BaseItem(), BaseItem))
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertEqual(warnings[0].category, ScrapyDeprecationWarning)
|
||||
|
||||
|
||||
class ItemNoDeprecationWarningTest(unittest.TestCase):
|
||||
def test_no_deprecation_warning(self):
|
||||
"""
|
||||
Make sure deprecation warnings are NOT logged whenever BaseItem subclasses are used.
|
||||
"""
|
||||
class SubclassedItem(Item):
|
||||
pass
|
||||
|
||||
with catch_warnings(record=True) as warnings:
|
||||
Item()
|
||||
SubclassedItem()
|
||||
_BaseItem()
|
||||
self.assertFalse(isinstance("foo", _BaseItem))
|
||||
self.assertFalse(isinstance("foo", Item))
|
||||
self.assertFalse(isinstance("foo", SubclassedItem))
|
||||
self.assertTrue(isinstance(_BaseItem(), _BaseItem))
|
||||
self.assertTrue(isinstance(Item(), Item))
|
||||
self.assertTrue(isinstance(SubclassedItem(), SubclassedItem))
|
||||
self.assertEqual(len(warnings), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import pickle
|
||||
import re
|
||||
import unittest
|
||||
from warnings import catch_warnings
|
||||
|
|
@ -171,9 +172,9 @@ class Base:
|
|||
self.assertEqual(lx.matches(url1), False)
|
||||
self.assertEqual(lx.matches(url2), True)
|
||||
|
||||
lx = self.extractor_cls(allow=('blah1',), deny=('blah2',),
|
||||
allow_domains=('blah1.com',),
|
||||
deny_domains=('blah2.com',))
|
||||
lx = self.extractor_cls(allow=['blah1'], deny=['blah2'],
|
||||
allow_domains=['blah1.com'],
|
||||
deny_domains=['blah2.com'])
|
||||
self.assertEqual(lx.matches('http://blah1.com/blah1'), True)
|
||||
self.assertEqual(lx.matches('http://blah1.com/blah2'), False)
|
||||
self.assertEqual(lx.matches('http://blah2.com/blah1'), False)
|
||||
|
|
@ -279,8 +280,8 @@ class Base:
|
|||
def test_process_value(self):
|
||||
"""Test restrict_xpaths with encodings"""
|
||||
html = b"""
|
||||
<a href="javascript:goToPage('../other/page.html','photo','width=600,height=540,scrollbars'); return false">Link text</a>
|
||||
<a href="/about.html">About us</a>
|
||||
<a href="javascript:goToPage('../other/page.html','photo','width=600,height=540,scrollbars'); return false">Text</a>
|
||||
<a href="/about.html">About us</a>
|
||||
"""
|
||||
response = HtmlResponse("http://example.org/somepage/index.html", body=html, encoding='windows-1252')
|
||||
|
||||
|
|
@ -291,7 +292,7 @@ class Base:
|
|||
|
||||
lx = self.extractor_cls(process_value=process_value)
|
||||
self.assertEqual(lx.extract_links(response),
|
||||
[Link(url='http://example.org/other/page.html', text='Link text')])
|
||||
[Link(url='http://example.org/other/page.html', text='Text')])
|
||||
|
||||
def test_base_url_with_restrict_xpaths(self):
|
||||
html = b"""<html><head><title>Page title<title><base href="http://otherdomain.com/base/" />
|
||||
|
|
@ -332,7 +333,10 @@ class Base:
|
|||
self.assertEqual(lx.extract_links(self.response), [])
|
||||
|
||||
def test_tags(self):
|
||||
html = b"""<html><area href="sample1.html"></area><a href="sample2.html">sample 2</a><img src="sample2.jpg"/></html>"""
|
||||
html = (
|
||||
b'<html><area href="sample1.html"></area>'
|
||||
b'<a href="sample2.html">sample 2</a><img src="sample2.jpg"/></html>'
|
||||
)
|
||||
response = HtmlResponse("http://example.com/index.html", body=html)
|
||||
|
||||
lx = self.extractor_cls(tags=None)
|
||||
|
|
@ -418,8 +422,10 @@ class Base:
|
|||
[
|
||||
Link(url='http://example.com/about.html', text=u'About us', fragment='', nofollow=False),
|
||||
Link(url='http://example.com/follow.html', text=u'Follow this link', fragment='', nofollow=False),
|
||||
Link(url='http://example.com/nofollow.html', text=u'Dont follow this one', fragment='', nofollow=True),
|
||||
Link(url='http://example.com/nofollow2.html', text=u'Choose to follow or not', fragment='', nofollow=False),
|
||||
Link(url='http://example.com/nofollow.html', text=u'Dont follow this one',
|
||||
fragment='', nofollow=True),
|
||||
Link(url='http://example.com/nofollow2.html', text=u'Choose to follow or not',
|
||||
fragment='', nofollow=False),
|
||||
Link(url='http://google.com/something', text=u'External link not to follow', nofollow=True),
|
||||
]
|
||||
)
|
||||
|
|
@ -432,8 +438,10 @@ class Base:
|
|||
[
|
||||
Link(url='http://example.com/about.html', text=u'About us', fragment='', nofollow=False),
|
||||
Link(url='http://example.com/follow.html', text=u'Follow this link', fragment='', nofollow=False),
|
||||
Link(url='http://example.com/nofollow.html', text=u'Dont follow this one', fragment='', nofollow=True),
|
||||
Link(url='http://example.com/nofollow2.html', text=u'Choose to follow or not', fragment='', nofollow=False),
|
||||
Link(url='http://example.com/nofollow.html', text=u'Dont follow this one',
|
||||
fragment='', nofollow=True),
|
||||
Link(url='http://example.com/nofollow2.html', text=u'Choose to follow or not',
|
||||
fragment='', nofollow=False),
|
||||
Link(url='http://google.com/something', text=u'External link not to follow', nofollow=True),
|
||||
]
|
||||
)
|
||||
|
|
@ -462,6 +470,10 @@ class Base:
|
|||
Link(url='ftp://www.external.com/', text=u'An Item', fragment='', nofollow=False),
|
||||
])
|
||||
|
||||
def test_pickle_extractor(self):
|
||||
lx = self.extractor_cls()
|
||||
self.assertIsInstance(pickle.loads(pickle.dumps(lx)), self.extractor_cls)
|
||||
|
||||
|
||||
class LxmlLinkExtractorTestCase(Base.LinkExtractorTestCase):
|
||||
extractor_cls = LxmlLinkExtractor
|
||||
|
|
|
|||
|
|
@ -601,7 +601,7 @@ class NoInputReprocessingItemLoader(BaseNoInputReprocessingLoader):
|
|||
|
||||
class NoInputReprocessingFromItemTest(unittest.TestCase):
|
||||
"""
|
||||
Loaders initialized from loaded items must not reprocess fields (BaseItem instances)
|
||||
Loaders initialized from loaded items must not reprocess fields (Item instances)
|
||||
"""
|
||||
def test_avoid_reprocessing_with_initial_values_single(self):
|
||||
il = NoInputReprocessingItemLoader(item=NoInputReprocessingItem(title='foo'))
|
||||
|
|
|
|||
|
|
@ -34,15 +34,15 @@ class LogFormatterTestCase(unittest.TestCase):
|
|||
res = Response("http://www.example.com")
|
||||
logkws = self.formatter.crawled(req, res, self.spider)
|
||||
logline = logkws['msg'] % logkws['args']
|
||||
self.assertEqual(logline,
|
||||
"Crawled (200) <GET http://www.example.com> (referer: None)")
|
||||
self.assertEqual(logline, "Crawled (200) <GET http://www.example.com> (referer: None)")
|
||||
|
||||
def test_crawled_without_referer(self):
|
||||
req = Request("http://www.example.com", headers={'referer': 'http://example.com'})
|
||||
res = Response("http://www.example.com", flags=['cached'])
|
||||
logkws = self.formatter.crawled(req, res, self.spider)
|
||||
logline = logkws['msg'] % logkws['args']
|
||||
self.assertEqual(logline,
|
||||
self.assertEqual(
|
||||
logline,
|
||||
"Crawled (200) <GET http://www.example.com> (referer: http://example.com) ['cached']")
|
||||
|
||||
def test_flags_in_request(self):
|
||||
|
|
@ -50,8 +50,9 @@ class LogFormatterTestCase(unittest.TestCase):
|
|||
res = Response("http://www.example.com")
|
||||
logkws = self.formatter.crawled(req, res, self.spider)
|
||||
logline = logkws['msg'] % logkws['args']
|
||||
self.assertEqual(logline,
|
||||
"Crawled (200) <GET http://www.example.com> ['test', 'flag'] (referer: None)")
|
||||
self.assertEqual(
|
||||
logline,
|
||||
"Crawled (200) <GET http://www.example.com> ['test', 'flag'] (referer: None)")
|
||||
|
||||
def test_dropped(self):
|
||||
item = {}
|
||||
|
|
@ -140,7 +141,8 @@ class LogformatterSubclassTest(LogFormatterTestCase):
|
|||
res = Response("http://www.example.com")
|
||||
logkws = self.formatter.crawled(req, res, self.spider)
|
||||
logline = logkws['msg'] % logkws['args']
|
||||
self.assertEqual(logline,
|
||||
self.assertEqual(
|
||||
logline,
|
||||
"Crawled (200) <GET http://www.example.com> (referer: None) []")
|
||||
|
||||
def test_crawled_without_referer(self):
|
||||
|
|
@ -148,7 +150,8 @@ class LogformatterSubclassTest(LogFormatterTestCase):
|
|||
res = Response("http://www.example.com")
|
||||
logkws = self.formatter.crawled(req, res, self.spider)
|
||||
logline = logkws['msg'] % logkws['args']
|
||||
self.assertEqual(logline,
|
||||
self.assertEqual(
|
||||
logline,
|
||||
"Crawled (200) <GET http://www.example.com> (referer: http://example.com) ['cached']")
|
||||
|
||||
def test_flags_in_request(self):
|
||||
|
|
@ -156,7 +159,9 @@ class LogformatterSubclassTest(LogFormatterTestCase):
|
|||
res = Response("http://www.example.com")
|
||||
logkws = self.formatter.crawled(req, res, self.spider)
|
||||
logline = logkws['msg'] % logkws['args']
|
||||
self.assertEqual(logline, "Crawled (200) <GET http://www.example.com> (referer: None) ['test', 'flag']")
|
||||
self.assertEqual(
|
||||
logline,
|
||||
"Crawled (200) <GET http://www.example.com> (referer: None) ['test', 'flag']")
|
||||
|
||||
|
||||
class SkipMessagesLogFormatter(LogFormatter):
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class MailSenderTest(unittest.TestCase):
|
|||
|
||||
mailsender = MailSender(debug=True)
|
||||
mailsender.send(to=['test@scrapy.org'], subject='subject', body='body',
|
||||
attachs=attachs, _callback=self._catch_mail_sent)
|
||||
attachs=attachs, _callback=self._catch_mail_sent)
|
||||
|
||||
assert self.catched_msg
|
||||
self.assertEqual(self.catched_msg['to'], ['test@scrapy.org'])
|
||||
|
|
|
|||
|
|
@ -69,11 +69,14 @@ class MiddlewareManagerTest(unittest.TestCase):
|
|||
|
||||
def test_methods(self):
|
||||
mwman = TestMiddlewareManager(M1(), M2(), M3())
|
||||
self.assertEqual([x.__self__.__class__ for x in mwman.methods['open_spider']],
|
||||
self.assertEqual(
|
||||
[x.__self__.__class__ for x in mwman.methods['open_spider']],
|
||||
[M1, M2])
|
||||
self.assertEqual([x.__self__.__class__ for x in mwman.methods['close_spider']],
|
||||
self.assertEqual(
|
||||
[x.__self__.__class__ for x in mwman.methods['close_spider']],
|
||||
[M2, M1])
|
||||
self.assertEqual([x.__self__.__class__ for x in mwman.methods['process']],
|
||||
self.assertEqual(
|
||||
[x.__self__.__class__ for x in mwman.methods['process']],
|
||||
[M1, M3])
|
||||
|
||||
def test_enabled(self):
|
||||
|
|
|
|||
|
|
@ -51,10 +51,10 @@ class FileDownloadCrawlTestCase(TestCase):
|
|||
store_setting_key = 'FILES_STORE'
|
||||
media_key = 'files'
|
||||
media_urls_key = 'file_urls'
|
||||
expected_checksums = set([
|
||||
expected_checksums = {
|
||||
'5547178b89448faf0015a13f904c936e',
|
||||
'c2281c83670e31d8aaab7cb642b824db',
|
||||
'ed3f6538dc15d4d9179dae57319edc5f'])
|
||||
'ed3f6538dc15d4d9179dae57319edc5f'}
|
||||
|
||||
def setUp(self):
|
||||
self.mockserver = MockServer()
|
||||
|
|
@ -91,6 +91,11 @@ class FileDownloadCrawlTestCase(TestCase):
|
|||
file_dl_success = 'File (downloaded): Downloaded file from'
|
||||
self.assertEqual(logs.count(file_dl_success), 3)
|
||||
|
||||
# check that the images/files status is `downloaded`
|
||||
for item in items:
|
||||
for i in item[self.media_key]:
|
||||
self.assertEqual(i['status'], 'downloaded')
|
||||
|
||||
# check that the images/files checksums are what we know they should be
|
||||
if self.expected_checksums is not None:
|
||||
checksums = set(
|
||||
|
|
@ -131,7 +136,8 @@ class FileDownloadCrawlTestCase(TestCase):
|
|||
def test_download_media(self):
|
||||
crawler = self._create_crawler(MediaDownloadSpider)
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/files/images/"),
|
||||
yield crawler.crawl(
|
||||
self.mockserver.url("/files/images/"),
|
||||
media_key=self.media_key,
|
||||
media_urls_key=self.media_urls_key)
|
||||
self._assert_files_downloaded(self.items, str(log))
|
||||
|
|
@ -140,7 +146,8 @@ class FileDownloadCrawlTestCase(TestCase):
|
|||
def test_download_media_wrong_urls(self):
|
||||
crawler = self._create_crawler(BrokenLinksMediaDownloadSpider)
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/files/images/"),
|
||||
yield crawler.crawl(
|
||||
self.mockserver.url("/files/images/"),
|
||||
media_key=self.media_key,
|
||||
media_urls_key=self.media_urls_key)
|
||||
self._assert_files_download_failure(crawler, self.items, 404, str(log))
|
||||
|
|
@ -149,7 +156,8 @@ class FileDownloadCrawlTestCase(TestCase):
|
|||
def test_download_media_redirected_default_failure(self):
|
||||
crawler = self._create_crawler(RedirectedMediaDownloadSpider)
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/files/images/"),
|
||||
yield crawler.crawl(
|
||||
self.mockserver.url("/files/images/"),
|
||||
media_key=self.media_key,
|
||||
media_urls_key=self.media_urls_key,
|
||||
mockserver=self.mockserver)
|
||||
|
|
@ -163,7 +171,8 @@ class FileDownloadCrawlTestCase(TestCase):
|
|||
|
||||
crawler = self._create_crawler(RedirectedMediaDownloadSpider)
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/files/images/"),
|
||||
yield crawler.crawl(
|
||||
self.mockserver.url("/files/images/"),
|
||||
media_key=self.media_key,
|
||||
media_urls_key=self.media_urls_key,
|
||||
mockserver=self.mockserver)
|
||||
|
|
|
|||
|
|
@ -38,27 +38,36 @@ class FilesPipelineTestCase(unittest.TestCase):
|
|||
|
||||
def test_file_path(self):
|
||||
file_path = self.pipeline.file_path
|
||||
self.assertEqual(file_path(Request("https://dev.mydeco.com/mydeco.pdf")),
|
||||
'full/c9b564df929f4bc635bdd19fde4f3d4847c757c5.pdf')
|
||||
self.assertEqual(file_path(Request("http://www.maddiebrown.co.uk///catalogue-items//image_54642_12175_95307.txt")),
|
||||
'full/4ce274dd83db0368bafd7e406f382ae088e39219.txt')
|
||||
self.assertEqual(file_path(Request("https://dev.mydeco.com/two/dirs/with%20spaces%2Bsigns.doc")),
|
||||
'full/94ccc495a17b9ac5d40e3eabf3afcb8c2c9b9e1a.doc')
|
||||
self.assertEqual(file_path(Request("http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg")),
|
||||
'full/4507be485f38b0da8a0be9eb2e1dfab8a19223f2.jpg')
|
||||
self.assertEqual(file_path(Request("http://www.dorma.co.uk/images/product_details/2532/")),
|
||||
'full/97ee6f8a46cbbb418ea91502fd24176865cf39b2')
|
||||
self.assertEqual(file_path(Request("http://www.dorma.co.uk/images/product_details/2532")),
|
||||
'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1')
|
||||
self.assertEqual(file_path(Request("http://www.dorma.co.uk/images/product_details/2532"),
|
||||
response=Response("http://www.dorma.co.uk/images/product_details/2532"),
|
||||
info=object()),
|
||||
'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1')
|
||||
self.assertEqual(file_path(Request("http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg.bohaha")),
|
||||
'full/76c00cef2ef669ae65052661f68d451162829507')
|
||||
self.assertEqual(file_path(Request("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAR0AAACxCAMAAADOHZloAAACClBMVEX/\
|
||||
self.assertEqual(
|
||||
file_path(Request("https://dev.mydeco.com/mydeco.pdf")),
|
||||
'full/c9b564df929f4bc635bdd19fde4f3d4847c757c5.pdf')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.maddiebrown.co.uk///catalogue-items//image_54642_12175_95307.txt")),
|
||||
'full/4ce274dd83db0368bafd7e406f382ae088e39219.txt')
|
||||
self.assertEqual(
|
||||
file_path(Request("https://dev.mydeco.com/two/dirs/with%20spaces%2Bsigns.doc")),
|
||||
'full/94ccc495a17b9ac5d40e3eabf3afcb8c2c9b9e1a.doc')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg")),
|
||||
'full/4507be485f38b0da8a0be9eb2e1dfab8a19223f2.jpg')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.dorma.co.uk/images/product_details/2532/")),
|
||||
'full/97ee6f8a46cbbb418ea91502fd24176865cf39b2')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.dorma.co.uk/images/product_details/2532")),
|
||||
'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.dorma.co.uk/images/product_details/2532"),
|
||||
response=Response("http://www.dorma.co.uk/images/product_details/2532"),
|
||||
info=object()),
|
||||
'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg.bohaha")),
|
||||
'full/76c00cef2ef669ae65052661f68d451162829507')
|
||||
self.assertEqual(
|
||||
file_path(Request("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAR0AAACxCAMAAADOHZloAAACClBMVEX/\
|
||||
//+F0tzCwMK76ZKQ21AMqr7oAAC96JvD5aWM2kvZ78J0N7fmAAC46Y4Ap7y")),
|
||||
'full/178059cbeba2e34120a67f2dc1afc3ecc09b61cb.png')
|
||||
'full/178059cbeba2e34120a67f2dc1afc3ecc09b61cb.png')
|
||||
|
||||
def test_fs_store(self):
|
||||
assert isinstance(self.pipeline.store, FSFilesStore)
|
||||
|
|
@ -84,6 +93,7 @@ class FilesPipelineTestCase(unittest.TestCase):
|
|||
|
||||
result = yield self.pipeline.process_item(item, None)
|
||||
self.assertEqual(result['files'][0]['checksum'], 'abc')
|
||||
self.assertEqual(result['files'][0]['status'], 'uptodate')
|
||||
|
||||
for p in patchers:
|
||||
p.stop()
|
||||
|
|
@ -105,6 +115,29 @@ class FilesPipelineTestCase(unittest.TestCase):
|
|||
|
||||
result = yield self.pipeline.process_item(item, None)
|
||||
self.assertNotEqual(result['files'][0]['checksum'], 'abc')
|
||||
self.assertEqual(result['files'][0]['status'], 'downloaded')
|
||||
|
||||
for p in patchers:
|
||||
p.stop()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_file_cached(self):
|
||||
item_url = "http://example.com/file3.pdf"
|
||||
item = _create_item_with_files(item_url)
|
||||
patchers = [
|
||||
mock.patch.object(FilesPipeline, 'inc_stats', return_value=True),
|
||||
mock.patch.object(FSFilesStore, 'stat_file', return_value={
|
||||
'checksum': 'abc',
|
||||
'last_modified': time.time() - (self.pipeline.expires * 60 * 60 * 24 * 2)}),
|
||||
mock.patch.object(FilesPipeline, 'get_media_requests',
|
||||
return_value=[_prepare_request_object(item_url, flags=['cached'])])
|
||||
]
|
||||
for p in patchers:
|
||||
p.start()
|
||||
|
||||
result = yield self.pipeline.process_item(item, None)
|
||||
self.assertNotEqual(result['files'][0]['checksum'], 'abc')
|
||||
self.assertEqual(result['files'][0]['status'], 'cached')
|
||||
|
||||
for p in patchers:
|
||||
p.stop()
|
||||
|
|
@ -403,10 +436,10 @@ def _create_item_with_files(*files):
|
|||
return item
|
||||
|
||||
|
||||
def _prepare_request_object(item_url):
|
||||
def _prepare_request_object(item_url, flags=None):
|
||||
return Request(
|
||||
item_url,
|
||||
meta={'response': Response(item_url, status=200, body=b'data')})
|
||||
meta={'response': Response(item_url, status=200, body=b'data', flags=flags)})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ from scrapy.utils.python import to_bytes
|
|||
skip = False
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as e:
|
||||
except ImportError:
|
||||
skip = 'Missing Python Imaging Library, install https://pypi.python.org/pypi/Pillow'
|
||||
else:
|
||||
encoders = set(('jpeg_encoder', 'jpeg_decoder'))
|
||||
encoders = {'jpeg_encoder', 'jpeg_decoder'}
|
||||
if not encoders.issubset(set(Image.core.__dict__)):
|
||||
skip = 'Missing JPEG encoders'
|
||||
|
||||
|
|
@ -41,22 +41,29 @@ class ImagesPipelineTestCase(unittest.TestCase):
|
|||
|
||||
def test_file_path(self):
|
||||
file_path = self.pipeline.file_path
|
||||
self.assertEqual(file_path(Request("https://dev.mydeco.com/mydeco.gif")),
|
||||
'full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg')
|
||||
self.assertEqual(file_path(Request("http://www.maddiebrown.co.uk///catalogue-items//image_54642_12175_95307.jpg")),
|
||||
'full/0ffcd85d563bca45e2f90becd0ca737bc58a00b2.jpg')
|
||||
self.assertEqual(file_path(Request("https://dev.mydeco.com/two/dirs/with%20spaces%2Bsigns.gif")),
|
||||
'full/b250e3a74fff2e4703e310048a5b13eba79379d2.jpg')
|
||||
self.assertEqual(file_path(Request("http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg")),
|
||||
'full/4507be485f38b0da8a0be9eb2e1dfab8a19223f2.jpg')
|
||||
self.assertEqual(file_path(Request("http://www.dorma.co.uk/images/product_details/2532/")),
|
||||
'full/97ee6f8a46cbbb418ea91502fd24176865cf39b2.jpg')
|
||||
self.assertEqual(file_path(Request("http://www.dorma.co.uk/images/product_details/2532")),
|
||||
'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1.jpg')
|
||||
self.assertEqual(file_path(Request("http://www.dorma.co.uk/images/product_details/2532"),
|
||||
response=Response("http://www.dorma.co.uk/images/product_details/2532"),
|
||||
info=object()),
|
||||
'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1.jpg')
|
||||
self.assertEqual(
|
||||
file_path(Request("https://dev.mydeco.com/mydeco.gif")),
|
||||
'full/3fd165099d8e71b8a48b2683946e64dbfad8b52d.jpg')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.maddiebrown.co.uk///catalogue-items//image_54642_12175_95307.jpg")),
|
||||
'full/0ffcd85d563bca45e2f90becd0ca737bc58a00b2.jpg')
|
||||
self.assertEqual(
|
||||
file_path(Request("https://dev.mydeco.com/two/dirs/with%20spaces%2Bsigns.gif")),
|
||||
'full/b250e3a74fff2e4703e310048a5b13eba79379d2.jpg')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.dfsonline.co.uk/get_prod_image.php?img=status_0907_mdm.jpg")),
|
||||
'full/4507be485f38b0da8a0be9eb2e1dfab8a19223f2.jpg')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.dorma.co.uk/images/product_details/2532/")),
|
||||
'full/97ee6f8a46cbbb418ea91502fd24176865cf39b2.jpg')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.dorma.co.uk/images/product_details/2532")),
|
||||
'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1.jpg')
|
||||
self.assertEqual(
|
||||
file_path(Request("http://www.dorma.co.uk/images/product_details/2532"),
|
||||
response=Response("http://www.dorma.co.uk/images/product_details/2532"),
|
||||
info=object()),
|
||||
'full/244e0dd7d96a3b7b01f54eded250c9e272577aa1.jpg')
|
||||
|
||||
def test_thumbnail_name(self):
|
||||
thumb_path = self.pipeline.thumb_path
|
||||
|
|
|
|||
|
|
@ -63,21 +63,21 @@ class BaseMediaPipelineTestCase(unittest.TestCase):
|
|||
fail = Failure(Exception())
|
||||
results = [(True, 1), (False, fail)]
|
||||
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
new_item = self.pipe.item_completed(results, item, self.info)
|
||||
|
||||
assert new_item is item
|
||||
assert len(l.records) == 1
|
||||
record = l.records[0]
|
||||
assert len(log.records) == 1
|
||||
record = log.records[0]
|
||||
assert record.levelname == 'ERROR'
|
||||
self.assertTupleEqual(record.exc_info, failure_to_exc_info(fail))
|
||||
|
||||
# disable failure logging and check again
|
||||
self.pipe.LOG_FAILED_RESULTS = False
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
new_item = self.pipe.item_completed(results, item, self.info)
|
||||
assert new_item is item
|
||||
assert len(l.records) == 0
|
||||
assert len(log.records) == 0
|
||||
|
||||
@inlineCallbacks
|
||||
def test_default_process_item(self):
|
||||
|
|
@ -214,9 +214,9 @@ class MediaPipelineTestCase(BaseMediaPipelineTestCase):
|
|||
item = dict(requests=req)
|
||||
new_item = yield self.pipe.process_item(item, self.spider)
|
||||
self.assertEqual(new_item['results'], [(True, rsp)])
|
||||
self.assertEqual(self.pipe._mockcalled,
|
||||
['get_media_requests', 'media_to_download',
|
||||
'media_downloaded', 'request_callback', 'item_completed'])
|
||||
self.assertEqual(
|
||||
self.pipe._mockcalled,
|
||||
['get_media_requests', 'media_to_download', 'media_downloaded', 'request_callback', 'item_completed'])
|
||||
|
||||
@inlineCallbacks
|
||||
def test_result_failure(self):
|
||||
|
|
@ -227,9 +227,9 @@ class MediaPipelineTestCase(BaseMediaPipelineTestCase):
|
|||
item = dict(requests=req)
|
||||
new_item = yield self.pipe.process_item(item, self.spider)
|
||||
self.assertEqual(new_item['results'], [(False, fail)])
|
||||
self.assertEqual(self.pipe._mockcalled,
|
||||
['get_media_requests', 'media_to_download',
|
||||
'media_failed', 'request_errback', 'item_completed'])
|
||||
self.assertEqual(
|
||||
self.pipe._mockcalled,
|
||||
['get_media_requests', 'media_to_download', 'media_failed', 'request_errback', 'item_completed'])
|
||||
|
||||
@inlineCallbacks
|
||||
def test_mix_of_success_and_failure(self):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import re
|
|||
import sys
|
||||
from subprocess import Popen, PIPE
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
from unittest import skipIf
|
||||
|
||||
import pytest
|
||||
from testfixtures import LogCapture
|
||||
|
|
@ -56,6 +57,8 @@ def _wrong_credentials(proxy_url):
|
|||
return urlunsplit(bad_auth_proxy)
|
||||
|
||||
|
||||
@skipIf(sys.version_info < (3, 5, 4),
|
||||
"requires mitmproxy < 3.0.0, which these tests do not support")
|
||||
class ProxyConnectTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
@ -76,35 +79,35 @@ class ProxyConnectTestCase(TestCase):
|
|||
@defer.inlineCallbacks
|
||||
def test_https_connect_tunnel(self):
|
||||
crawler = get_crawler(SimpleSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/status?n=200", is_secure=True))
|
||||
self._assert_got_response_code(200, l)
|
||||
self._assert_got_response_code(200, log)
|
||||
|
||||
@pytest.mark.xfail(reason='Python 3.6+ fails this earlier', condition=sys.version_info.minor >= 6)
|
||||
@pytest.mark.xfail(reason='Python 3.6+ fails this earlier', condition=sys.version_info >= (3, 6))
|
||||
@defer.inlineCallbacks
|
||||
def test_https_connect_tunnel_error(self):
|
||||
crawler = get_crawler(SimpleSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl("https://localhost:99999/status?n=200")
|
||||
self._assert_got_tunnel_error(l)
|
||||
self._assert_got_tunnel_error(log)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_https_tunnel_auth_error(self):
|
||||
os.environ['https_proxy'] = _wrong_credentials(os.environ['https_proxy'])
|
||||
crawler = get_crawler(SimpleSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(self.mockserver.url("/status?n=200", is_secure=True))
|
||||
# The proxy returns a 407 error code but it does not reach the client;
|
||||
# he just sees a TunnelError.
|
||||
self._assert_got_tunnel_error(l)
|
||||
self._assert_got_tunnel_error(log)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_https_tunnel_without_leak_proxy_authorization_header(self):
|
||||
request = Request(self.mockserver.url("/echo", is_secure=True))
|
||||
crawler = get_crawler(SingleRequestSpider)
|
||||
with LogCapture() as l:
|
||||
with LogCapture() as log:
|
||||
yield crawler.crawl(seed=request)
|
||||
self._assert_got_response_code(200, l)
|
||||
self._assert_got_response_code(200, log)
|
||||
echo = json.loads(crawler.spider.meta['responses'][0].text)
|
||||
self.assertTrue('Proxy-Authorization' not in echo['headers'])
|
||||
|
||||
|
|
|
|||
|
|
@ -158,6 +158,12 @@ class CallbackKeywordArgumentsTestCase(TestCase):
|
|||
if key in line.getMessage():
|
||||
exceptions[key] = line
|
||||
self.assertEqual(exceptions['takes_less'].exc_info[0], TypeError)
|
||||
self.assertEqual(str(exceptions['takes_less'].exc_info[1]), "parse_takes_less() got an unexpected keyword argument 'number'")
|
||||
self.assertEqual(
|
||||
str(exceptions['takes_less'].exc_info[1]),
|
||||
"parse_takes_less() got an unexpected keyword argument 'number'"
|
||||
)
|
||||
self.assertEqual(exceptions['takes_more'].exc_info[0], TypeError)
|
||||
self.assertEqual(str(exceptions['takes_more'].exc_info[1]), "parse_takes_more() missing 1 required positional argument: 'other'")
|
||||
self.assertEqual(
|
||||
str(exceptions['takes_more'].exc_info[1]),
|
||||
"parse_takes_more() missing 1 required positional argument: 'other'"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,8 +63,9 @@ class ResponseTypesTest(unittest.TestCase):
|
|||
def test_from_headers(self):
|
||||
mappings = [
|
||||
({'Content-Type': ['text/html; charset=utf-8']}, HtmlResponse),
|
||||
({'Content-Type': ['application/octet-stream'], 'Content-Disposition': ['attachment; filename=data.txt']}, TextResponse),
|
||||
({'Content-Type': ['text/html; charset=utf-8'], 'Content-Encoding': ['gzip']}, Response),
|
||||
({'Content-Type': ['application/octet-stream'],
|
||||
'Content-Disposition': ['attachment; filename=data.txt']}, TextResponse),
|
||||
]
|
||||
for source, cls in mappings:
|
||||
source = Headers(source)
|
||||
|
|
@ -76,8 +77,10 @@ class ResponseTypesTest(unittest.TestCase):
|
|||
mappings = [
|
||||
({'url': 'http://www.example.com/data.csv'}, TextResponse),
|
||||
# headers takes precedence over url
|
||||
({'headers': Headers({'Content-Type': ['text/html; charset=utf-8']}), 'url': 'http://www.example.com/item/'}, HtmlResponse),
|
||||
({'headers': Headers({'Content-Disposition': ['attachment; filename="data.xml.gz"']}), 'url': 'http://www.example.com/page/'}, Response),
|
||||
({'headers': Headers({'Content-Type': ['text/html; charset=utf-8']}),
|
||||
'url': 'http://www.example.com/item/'}, HtmlResponse),
|
||||
({'headers': Headers({'Content-Disposition': ['attachment; filename="data.xml.gz"']}),
|
||||
'url': 'http://www.example.com/page/'}, Response),
|
||||
|
||||
|
||||
]
|
||||
|
|
|
|||
|
|
@ -19,18 +19,26 @@ class SelectorTestCase(unittest.TestCase):
|
|||
for x in xl:
|
||||
assert isinstance(x, Selector)
|
||||
|
||||
self.assertEqual(sel.xpath('//input').getall(),
|
||||
[x.get() for x in sel.xpath('//input')])
|
||||
|
||||
self.assertEqual([x.get() for x in sel.xpath("//input[@name='a']/@name")],
|
||||
[u'a'])
|
||||
self.assertEqual([x.get() for x in sel.xpath("number(concat(//input[@name='a']/@value, //input[@name='b']/@value))")],
|
||||
[u'12.0'])
|
||||
|
||||
self.assertEqual(sel.xpath("concat('xpath', 'rules')").getall(),
|
||||
[u'xpathrules'])
|
||||
self.assertEqual([x.get() for x in sel.xpath("concat(//input[@name='a']/@value, //input[@name='b']/@value)")],
|
||||
[u'12'])
|
||||
self.assertEqual(
|
||||
sel.xpath('//input').getall(),
|
||||
[x.get() for x in sel.xpath('//input')]
|
||||
)
|
||||
self.assertEqual(
|
||||
[x.get() for x in sel.xpath("//input[@name='a']/@name")],
|
||||
[u'a']
|
||||
)
|
||||
self.assertEqual(
|
||||
[x.get() for x in sel.xpath("number(concat(//input[@name='a']/@value, //input[@name='b']/@value))")],
|
||||
[u'12.0']
|
||||
)
|
||||
self.assertEqual(
|
||||
sel.xpath("concat('xpath', 'rules')").getall(),
|
||||
[u'xpathrules']
|
||||
)
|
||||
self.assertEqual(
|
||||
[x.get() for x in sel.xpath("concat(//input[@name='a']/@value, //input[@name='b']/@value)")],
|
||||
[u'12']
|
||||
)
|
||||
|
||||
def test_root_base_url(self):
|
||||
body = b'<html><form action="/path"><input name="a" /></form></html>'
|
||||
|
|
|
|||
|
|
@ -314,13 +314,17 @@ class BaseSettingsTest(unittest.TestCase):
|
|||
'TEST_BASE': BaseSettings({1: 1, 2: 2}, 'project'),
|
||||
'TEST': BaseSettings({1: 10, 3: 30}, 'default'),
|
||||
'HASNOBASE': BaseSettings({3: 3000}, 'default')})
|
||||
self.assertDictEqual(s.copy_to_dict(),
|
||||
{'HASNOBASE': {3: 3000},
|
||||
'TEST': {1: 10, 3: 30},
|
||||
'TEST_BASE': {1: 1, 2: 2},
|
||||
'TEST_BOOLEAN': False,
|
||||
'TEST_LIST': [1, 2],
|
||||
'TEST_STRING': 'a string'})
|
||||
self.assertDictEqual(
|
||||
s.copy_to_dict(),
|
||||
{
|
||||
'HASNOBASE': {3: 3000},
|
||||
'TEST': {1: 10, 3: 30},
|
||||
'TEST_BASE': {1: 1, 2: 2},
|
||||
'TEST_LIST': [1, 2],
|
||||
'TEST_BOOLEAN': False,
|
||||
'TEST_STRING': 'a string',
|
||||
}
|
||||
)
|
||||
|
||||
def test_freeze(self):
|
||||
self.settings.freeze()
|
||||
|
|
|
|||
|
|
@ -120,7 +120,9 @@ class XMLFeedSpiderTest(SpiderTest):
|
|||
body = b"""<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns:x="http://www.google.com/schemas/sitemap/0.84"
|
||||
xmlns:y="http://www.example.com/schemas/extras/1.0">
|
||||
<url><x:loc>http://www.example.com/Special-Offers.html</loc><y:updated>2009-08-16</updated><other value="bar" y:custom="fuu"/></url>
|
||||
<url><x:loc>http://www.example.com/Special-Offers.html</loc><y:updated>2009-08-16</updated>
|
||||
<other value="bar" y:custom="fuu"/>
|
||||
</url>
|
||||
<url><loc>http://www.example.com/</loc><y:updated>2009-08-16</updated><other value="foo"/></url>
|
||||
</urlset>"""
|
||||
response = XmlResponse(url='http://example.com/sitemap.xml', body=body)
|
||||
|
|
|
|||
|
|
@ -40,25 +40,32 @@ class SpiderLoaderTest(unittest.TestCase):
|
|||
verifyObject(ISpiderLoader, self.spider_loader)
|
||||
|
||||
def test_list(self):
|
||||
self.assertEqual(set(self.spider_loader.list()),
|
||||
set(['spider1', 'spider2', 'spider3', 'spider4']))
|
||||
self.assertEqual(
|
||||
set(self.spider_loader.list()),
|
||||
{'spider1', 'spider2', 'spider3', 'spider4'})
|
||||
|
||||
def test_load(self):
|
||||
spider1 = self.spider_loader.load("spider1")
|
||||
self.assertEqual(spider1.__name__, 'Spider1')
|
||||
|
||||
def test_find_by_request(self):
|
||||
self.assertEqual(self.spider_loader.find_by_request(Request('http://scrapy1.org/test')),
|
||||
self.assertEqual(
|
||||
self.spider_loader.find_by_request(Request('http://scrapy1.org/test')),
|
||||
['spider1'])
|
||||
self.assertEqual(self.spider_loader.find_by_request(Request('http://scrapy2.org/test')),
|
||||
self.assertEqual(
|
||||
self.spider_loader.find_by_request(Request('http://scrapy2.org/test')),
|
||||
['spider2'])
|
||||
self.assertEqual(set(self.spider_loader.find_by_request(Request('http://scrapy3.org/test'))),
|
||||
set(['spider1', 'spider2']))
|
||||
self.assertEqual(self.spider_loader.find_by_request(Request('http://scrapy999.org/test')),
|
||||
self.assertEqual(
|
||||
set(self.spider_loader.find_by_request(Request('http://scrapy3.org/test'))),
|
||||
{'spider1', 'spider2'})
|
||||
self.assertEqual(
|
||||
self.spider_loader.find_by_request(Request('http://scrapy999.org/test')),
|
||||
[])
|
||||
self.assertEqual(self.spider_loader.find_by_request(Request('http://spider3.com')),
|
||||
self.assertEqual(
|
||||
self.spider_loader.find_by_request(Request('http://spider3.com')),
|
||||
[])
|
||||
self.assertEqual(self.spider_loader.find_by_request(Request('http://spider3.com/onlythis')),
|
||||
self.assertEqual(
|
||||
self.spider_loader.find_by_request(Request('http://spider3.com/onlythis')),
|
||||
['spider3'])
|
||||
|
||||
def test_load_spider_module(self):
|
||||
|
|
@ -137,9 +144,14 @@ class DuplicateSpiderNameLoaderTest(unittest.TestCase):
|
|||
msg = str(w[0].message)
|
||||
self.assertIn("several spiders with the same name", msg)
|
||||
self.assertIn("'spider3'", msg)
|
||||
self.assertTrue(msg.count("'spider3'") == 2)
|
||||
|
||||
self.assertNotIn("'spider1'", msg)
|
||||
self.assertNotIn("'spider2'", msg)
|
||||
self.assertNotIn("'spider4'", msg)
|
||||
|
||||
spiders = set(spider_loader.list())
|
||||
self.assertEqual(spiders, set(['spider1', 'spider2', 'spider3', 'spider4']))
|
||||
self.assertEqual(spiders, {'spider1', 'spider2', 'spider3', 'spider4'})
|
||||
|
||||
def test_multiple_dupename_warning(self):
|
||||
# copy 2 spider modules so as to have duplicate spider name
|
||||
|
|
@ -156,7 +168,13 @@ class DuplicateSpiderNameLoaderTest(unittest.TestCase):
|
|||
msg = str(w[0].message)
|
||||
self.assertIn("several spiders with the same name", msg)
|
||||
self.assertIn("'spider1'", msg)
|
||||
self.assertTrue(msg.count("'spider1'") == 2)
|
||||
|
||||
self.assertIn("'spider2'", msg)
|
||||
self.assertTrue(msg.count("'spider2'") == 2)
|
||||
|
||||
self.assertNotIn("'spider3'", msg)
|
||||
self.assertNotIn("'spider4'", msg)
|
||||
|
||||
spiders = set(spider_loader.list())
|
||||
self.assertEqual(spiders, set(['spider1', 'spider2', 'spider3', 'spider4']))
|
||||
self.assertEqual(spiders, {'spider1', 'spider2', 'spider3', 'spider4'})
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@ class _HttpErrorSpider(MockServerSpider):
|
|||
def __init__(self, *args, **kwargs):
|
||||
super(_HttpErrorSpider, self).__init__(*args, **kwargs)
|
||||
self.start_urls = [
|
||||
self.mockserver.url("/status?n=200"),
|
||||
self.mockserver.url("/status?n=404"),
|
||||
self.mockserver.url("/status?n=402"),
|
||||
self.mockserver.url("/status?n=500"),
|
||||
self.mockserver.url("/status?n=200"),
|
||||
self.mockserver.url("/status?n=404"),
|
||||
self.mockserver.url("/status?n=402"),
|
||||
self.mockserver.url("/status?n=500"),
|
||||
]
|
||||
self.failed = set()
|
||||
self.skipped = set()
|
||||
|
|
@ -68,29 +68,23 @@ class TestHttpErrorMiddleware(TestCase):
|
|||
self.res200, self.res404 = _responses(self.req, [200, 404])
|
||||
|
||||
def test_process_spider_input(self):
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(self.res200, self.spider))
|
||||
self.assertRaises(HttpError,
|
||||
self.mw.process_spider_input, self.res404, self.spider)
|
||||
self.assertIsNone(self.mw.process_spider_input(self.res200, self.spider))
|
||||
self.assertRaises(HttpError, self.mw.process_spider_input, self.res404, self.spider)
|
||||
|
||||
def test_process_spider_exception(self):
|
||||
self.assertEqual([],
|
||||
self.mw.process_spider_exception(self.res404,
|
||||
HttpError(self.res404), self.spider))
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_exception(self.res404,
|
||||
Exception(), self.spider))
|
||||
self.assertEqual(
|
||||
[],
|
||||
self.mw.process_spider_exception(self.res404, HttpError(self.res404), self.spider))
|
||||
self.assertIsNone(self.mw.process_spider_exception(self.res404, Exception(), self.spider))
|
||||
|
||||
def test_handle_httpstatus_list(self):
|
||||
res = self.res404.copy()
|
||||
res.request = Request('http://scrapytest.org',
|
||||
meta={'handle_httpstatus_list': [404]})
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(res, self.spider))
|
||||
self.assertIsNone(self.mw.process_spider_input(res, self.spider))
|
||||
|
||||
self.spider.handle_httpstatus_list = [404]
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(self.res404, self.spider))
|
||||
self.assertIsNone(self.mw.process_spider_input(self.res404, self.spider))
|
||||
|
||||
|
||||
class TestHttpErrorMiddlewareSettings(TestCase):
|
||||
|
|
@ -103,12 +97,9 @@ class TestHttpErrorMiddlewareSettings(TestCase):
|
|||
self.res200, self.res404, self.res402 = _responses(self.req, [200, 404, 402])
|
||||
|
||||
def test_process_spider_input(self):
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(self.res200, self.spider))
|
||||
self.assertRaises(HttpError,
|
||||
self.mw.process_spider_input, self.res404, self.spider)
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(self.res402, self.spider))
|
||||
self.assertIsNone(self.mw.process_spider_input(self.res200, self.spider))
|
||||
self.assertRaises(HttpError, self.mw.process_spider_input, self.res404, self.spider)
|
||||
self.assertIsNone(self.mw.process_spider_input(self.res402, self.spider))
|
||||
|
||||
def test_meta_overrides_settings(self):
|
||||
request = Request('http://scrapytest.org', meta={'handle_httpstatus_list': [404]})
|
||||
|
|
@ -117,17 +108,13 @@ class TestHttpErrorMiddlewareSettings(TestCase):
|
|||
res402 = self.res402.copy()
|
||||
res402.request = request
|
||||
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(res404, self.spider))
|
||||
self.assertRaises(HttpError,
|
||||
self.mw.process_spider_input, res402, self.spider)
|
||||
self.assertIsNone(self.mw.process_spider_input(res404, self.spider))
|
||||
self.assertRaises(HttpError, self.mw.process_spider_input, res402, self.spider)
|
||||
|
||||
def test_spider_override_settings(self):
|
||||
self.spider.handle_httpstatus_list = [404]
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(self.res404, self.spider))
|
||||
self.assertRaises(HttpError,
|
||||
self.mw.process_spider_input, self.res402, self.spider)
|
||||
self.assertIsNone(self.mw.process_spider_input(self.res404, self.spider))
|
||||
self.assertRaises(HttpError, self.mw.process_spider_input, self.res402, self.spider)
|
||||
|
||||
|
||||
class TestHttpErrorMiddlewareHandleAll(TestCase):
|
||||
|
|
@ -139,10 +126,8 @@ class TestHttpErrorMiddlewareHandleAll(TestCase):
|
|||
self.res200, self.res404, self.res402 = _responses(self.req, [200, 404, 402])
|
||||
|
||||
def test_process_spider_input(self):
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(self.res200, self.spider))
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(self.res404, self.spider))
|
||||
self.assertIsNone(self.mw.process_spider_input(self.res200, self.spider))
|
||||
self.assertIsNone(self.mw.process_spider_input(self.res404, self.spider))
|
||||
|
||||
def test_meta_overrides_settings(self):
|
||||
request = Request('http://scrapytest.org', meta={'handle_httpstatus_list': [404]})
|
||||
|
|
@ -151,10 +136,8 @@ class TestHttpErrorMiddlewareHandleAll(TestCase):
|
|||
res402 = self.res402.copy()
|
||||
res402.request = request
|
||||
|
||||
self.assertEqual(None,
|
||||
self.mw.process_spider_input(res404, self.spider))
|
||||
self.assertRaises(HttpError,
|
||||
self.mw.process_spider_input, res402, self.spider)
|
||||
self.assertIsNone(self.mw.process_spider_input(res404, self.spider))
|
||||
self.assertRaises(HttpError, self.mw.process_spider_input, res402, self.spider)
|
||||
|
||||
|
||||
class TestHttpErrorMiddlewareIntegrational(TrialTestCase):
|
||||
|
|
|
|||
|
|
@ -22,20 +22,24 @@ class TestOffsiteMiddleware(TestCase):
|
|||
def test_process_spider_output(self):
|
||||
res = Response('http://scrapytest.org')
|
||||
|
||||
onsite_reqs = [Request('http://scrapytest.org/1'),
|
||||
Request('http://scrapy.org/1'),
|
||||
Request('http://sub.scrapy.org/1'),
|
||||
Request('http://offsite.tld/letmepass', dont_filter=True),
|
||||
Request('http://scrapy.test.org/'),
|
||||
Request('http://scrapy.test.org:8000/')]
|
||||
offsite_reqs = [Request('http://scrapy2.org'),
|
||||
Request('http://offsite.tld/'),
|
||||
Request('http://offsite.tld/scrapytest.org'),
|
||||
Request('http://offsite.tld/rogue.scrapytest.org'),
|
||||
Request('http://rogue.scrapytest.org.haha.com'),
|
||||
Request('http://roguescrapytest.org'),
|
||||
Request('http://test.org/'),
|
||||
Request('http://notscrapy.test.org/')]
|
||||
onsite_reqs = [
|
||||
Request('http://scrapytest.org/1'),
|
||||
Request('http://scrapy.org/1'),
|
||||
Request('http://sub.scrapy.org/1'),
|
||||
Request('http://offsite.tld/letmepass', dont_filter=True),
|
||||
Request('http://scrapy.test.org/'),
|
||||
Request('http://scrapy.test.org:8000/'),
|
||||
]
|
||||
offsite_reqs = [
|
||||
Request('http://scrapy2.org'),
|
||||
Request('http://offsite.tld/'),
|
||||
Request('http://offsite.tld/scrapytest.org'),
|
||||
Request('http://offsite.tld/rogue.scrapytest.org'),
|
||||
Request('http://rogue.scrapytest.org.haha.com'),
|
||||
Request('http://roguescrapytest.org'),
|
||||
Request('http://test.org/'),
|
||||
Request('http://notscrapy.test.org/'),
|
||||
]
|
||||
reqs = onsite_reqs + offsite_reqs
|
||||
|
||||
out = list(self.mw.process_spider_output(res, reqs, self.spider))
|
||||
|
|
|
|||
|
|
@ -385,9 +385,15 @@ class TestSpiderMiddleware(TestCase):
|
|||
log4 = yield self.crawl_log(GeneratorOutputChainSpider)
|
||||
self.assertIn("'item_scraped_count': 2", str(log4))
|
||||
self.assertIn("GeneratorRecoverMiddleware.process_spider_exception: LookupError caught", str(log4))
|
||||
self.assertIn("GeneratorDoNothingAfterFailureMiddleware.process_spider_exception: LookupError caught", str(log4))
|
||||
self.assertNotIn("GeneratorFailMiddleware.process_spider_exception: LookupError caught", str(log4))
|
||||
self.assertNotIn("GeneratorDoNothingAfterRecoveryMiddleware.process_spider_exception: LookupError caught", str(log4))
|
||||
self.assertIn(
|
||||
"GeneratorDoNothingAfterFailureMiddleware.process_spider_exception: LookupError caught",
|
||||
str(log4))
|
||||
self.assertNotIn(
|
||||
"GeneratorFailMiddleware.process_spider_exception: LookupError caught",
|
||||
str(log4))
|
||||
self.assertNotIn(
|
||||
"GeneratorDoNothingAfterRecoveryMiddleware.process_spider_exception: LookupError caught",
|
||||
str(log4))
|
||||
item_from_callback = {'processed': [
|
||||
'parse-first-item',
|
||||
'GeneratorFailMiddleware.process_spider_output',
|
||||
|
|
@ -414,9 +420,13 @@ class TestSpiderMiddleware(TestCase):
|
|||
log5 = yield self.crawl_log(NotGeneratorOutputChainSpider)
|
||||
self.assertIn("'item_scraped_count': 1", str(log5))
|
||||
self.assertIn("GeneratorRecoverMiddleware.process_spider_exception: ReferenceError caught", str(log5))
|
||||
self.assertIn("GeneratorDoNothingAfterFailureMiddleware.process_spider_exception: ReferenceError caught", str(log5))
|
||||
self.assertIn(
|
||||
"GeneratorDoNothingAfterFailureMiddleware.process_spider_exception: ReferenceError caught",
|
||||
str(log5))
|
||||
self.assertNotIn("GeneratorFailMiddleware.process_spider_exception: ReferenceError caught", str(log5))
|
||||
self.assertNotIn("GeneratorDoNothingAfterRecoveryMiddleware.process_spider_exception: ReferenceError caught", str(log5))
|
||||
self.assertNotIn(
|
||||
"GeneratorDoNothingAfterRecoveryMiddleware.process_spider_exception: ReferenceError caught",
|
||||
str(log5))
|
||||
item_recovered = {'processed': [
|
||||
'NotGeneratorRecoverMiddleware.process_spider_exception',
|
||||
'NotGeneratorDoNothingAfterRecoveryMiddleware.process_spider_output']}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,11 @@ class MixinSameOrigin:
|
|||
('https://example.com:443/page.html', 'https://example.com/not-page.html', b'https://example.com/page.html'),
|
||||
('http://example.com:80/page.html', 'http://example.com/not-page.html', b'http://example.com/page.html'),
|
||||
('http://example.com/page.html', 'http://example.com:80/not-page.html', b'http://example.com/page.html'),
|
||||
('http://example.com:8888/page.html', 'http://example.com:8888/not-page.html', b'http://example.com:8888/page.html'),
|
||||
(
|
||||
'http://example.com:8888/page.html',
|
||||
'http://example.com:8888/not-page.html',
|
||||
b'http://example.com:8888/page.html',
|
||||
),
|
||||
|
||||
# Different host: do NOT send referrer
|
||||
('https://example.com/page.html', 'https://not.example.com/otherpage.html', None),
|
||||
|
|
@ -139,8 +143,12 @@ class MixinSameOrigin:
|
|||
('ftps://example.com/urls.zip', 'https://example.com/not-page.html', None),
|
||||
|
||||
# test for user/password stripping
|
||||
('https://user:password@example.com/page.html', 'https://example.com/not-page.html', b'https://example.com/page.html'),
|
||||
('https://user:password@example.com/page.html', 'http://example.com/not-page.html', None),
|
||||
(
|
||||
'https://user:password@example.com/page.html',
|
||||
'https://example.com/not-page.html',
|
||||
b'https://example.com/page.html',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -184,7 +192,11 @@ class MixinOriginWhenCrossOrigin:
|
|||
('https://example.com:443/page.html', 'https://example.com/not-page.html', b'https://example.com/page.html'),
|
||||
('http://example.com:80/page.html', 'http://example.com/not-page.html', b'http://example.com/page.html'),
|
||||
('http://example.com/page.html', 'http://example.com:80/not-page.html', b'http://example.com/page.html'),
|
||||
('http://example.com:8888/page.html', 'http://example.com:8888/not-page.html', b'http://example.com:8888/page.html'),
|
||||
(
|
||||
'http://example.com:8888/page.html',
|
||||
'http://example.com:8888/not-page.html',
|
||||
b'http://example.com:8888/page.html',
|
||||
),
|
||||
|
||||
# Different host: send origin as referrer
|
||||
('https://example2.com/page.html', 'https://scrapy.org/otherpage.html', b'https://example2.com/'),
|
||||
|
|
@ -205,9 +217,17 @@ class MixinOriginWhenCrossOrigin:
|
|||
('ftps://example4.com/urls.zip', 'https://example4.com/not-page.html', b'ftps://example4.com/'),
|
||||
|
||||
# test for user/password stripping
|
||||
('https://user:password@example5.com/page.html', 'https://example5.com/not-page.html', b'https://example5.com/page.html'),
|
||||
(
|
||||
'https://user:password@example5.com/page.html',
|
||||
'https://example5.com/not-page.html',
|
||||
b'https://example5.com/page.html',
|
||||
),
|
||||
# TLS to non-TLS downgrade: send origin
|
||||
('https://user:password@example5.com/page.html', 'http://example5.com/not-page.html', b'https://example5.com/'),
|
||||
(
|
||||
'https://user:password@example5.com/page.html',
|
||||
'http://example5.com/not-page.html',
|
||||
b'https://example5.com/',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -219,7 +239,11 @@ class MixinStrictOriginWhenCrossOrigin:
|
|||
('https://example.com:443/page.html', 'https://example.com/not-page.html', b'https://example.com/page.html'),
|
||||
('http://example.com:80/page.html', 'http://example.com/not-page.html', b'http://example.com/page.html'),
|
||||
('http://example.com/page.html', 'http://example.com:80/not-page.html', b'http://example.com/page.html'),
|
||||
('http://example.com:8888/page.html', 'http://example.com:8888/not-page.html', b'http://example.com:8888/page.html'),
|
||||
(
|
||||
'http://example.com:8888/page.html',
|
||||
'http://example.com:8888/not-page.html',
|
||||
b'http://example.com:8888/page.html',
|
||||
),
|
||||
|
||||
# Different host: send origin as referrer
|
||||
('https://example2.com/page.html', 'https://scrapy.org/otherpage.html', b'https://example2.com/'),
|
||||
|
|
@ -248,7 +272,11 @@ class MixinStrictOriginWhenCrossOrigin:
|
|||
('ftps://example4.com/urls.zip', 'https://example4.com/not-page.html', b'ftps://example4.com/'),
|
||||
|
||||
# test for user/password stripping
|
||||
('https://user:password@example5.com/page.html', 'https://example5.com/not-page.html', b'https://example5.com/page.html'),
|
||||
(
|
||||
'https://user:password@example5.com/page.html',
|
||||
'https://example5.com/not-page.html',
|
||||
b'https://example5.com/page.html',
|
||||
),
|
||||
|
||||
# TLS to non-TLS downgrade: send nothing
|
||||
('https://user:password@example5.com/page.html', 'http://example5.com/not-page.html', None),
|
||||
|
|
@ -281,8 +309,16 @@ class MixinUnsafeUrl:
|
|||
('ftp://example3.com/urls.zip', 'https://scrapy.org/', b'ftp://example3.com/urls.zip'),
|
||||
|
||||
# test for user/password stripping
|
||||
('http://user:password@example4.com/page.html', 'https://not.example4.com/', b'http://example4.com/page.html'),
|
||||
('https://user:password@example4.com/page.html', 'http://scrapy.org/', b'https://example4.com/page.html'),
|
||||
(
|
||||
'http://user:password@example4.com/page.html',
|
||||
'https://not.example4.com/',
|
||||
b'http://example4.com/page.html',
|
||||
),
|
||||
(
|
||||
'https://user:password@example4.com/page.html',
|
||||
'http://scrapy.org/',
|
||||
b'https://example4.com/page.html',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -459,7 +495,6 @@ class TestRequestMetaSettingFallback(TestCase):
|
|||
target = 'http://www.example.com'
|
||||
|
||||
for settings, response_headers, request_meta, policy_class, check_warning in self.params[3:]:
|
||||
spider = Spider('foo')
|
||||
mw = RefererMiddleware(Settings(settings))
|
||||
|
||||
response = Response(origin, headers=response_headers)
|
||||
|
|
@ -511,7 +546,7 @@ class TestSettingsPolicyByName(TestCase):
|
|||
def test_invalid_name(self):
|
||||
settings = Settings({'REFERRER_POLICY': 'some-custom-unknown-policy'})
|
||||
with self.assertRaises(RuntimeError):
|
||||
mw = RefererMiddleware(settings)
|
||||
RefererMiddleware(settings)
|
||||
|
||||
|
||||
class TestPolicyHeaderPredecence001(MixinUnsafeUrl, TestRefererMiddleware):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import pickle
|
||||
import sys
|
||||
|
||||
from queuelib.tests import test_queue as t
|
||||
from scrapy.squeues import (
|
||||
|
|
@ -28,31 +29,13 @@ class TestLoader(ItemLoader):
|
|||
|
||||
def nonserializable_object_test(self):
|
||||
q = self.queue()
|
||||
try:
|
||||
pickle.dumps(lambda x: x)
|
||||
except Exception:
|
||||
# Trigger Twisted bug #7989
|
||||
import twisted.persisted.styles # NOQA
|
||||
self.assertRaises(ValueError, q.push, lambda x: x)
|
||||
else:
|
||||
# Use a different unpickleable object
|
||||
class A:
|
||||
pass
|
||||
|
||||
a = A()
|
||||
a.__reduce__ = a.__reduce_ex__ = None
|
||||
self.assertRaises(ValueError, q.push, a)
|
||||
self.assertRaises(ValueError, q.push, lambda x: x)
|
||||
# Selectors should fail (lxml.html.HtmlElement objects can't be pickled)
|
||||
sel = Selector(text='<html><body><p>some text</p></body></html>')
|
||||
self.assertRaises(ValueError, q.push, sel)
|
||||
|
||||
|
||||
class MarshalFifoDiskQueueTest(t.FifoDiskQueueTest):
|
||||
|
||||
chunksize = 100000
|
||||
|
||||
def queue(self):
|
||||
return MarshalFifoDiskQueue(self.qpath, chunksize=self.chunksize)
|
||||
class FifoDiskQueueTestMixin:
|
||||
|
||||
def test_serialize(self):
|
||||
q = self.queue()
|
||||
|
|
@ -66,6 +49,13 @@ class MarshalFifoDiskQueueTest(t.FifoDiskQueueTest):
|
|||
test_nonserializable_object = nonserializable_object_test
|
||||
|
||||
|
||||
class MarshalFifoDiskQueueTest(t.FifoDiskQueueTest, FifoDiskQueueTestMixin):
|
||||
chunksize = 100000
|
||||
|
||||
def queue(self):
|
||||
return MarshalFifoDiskQueue(self.qpath, chunksize=self.chunksize)
|
||||
|
||||
|
||||
class ChunkSize1MarshalFifoDiskQueueTest(MarshalFifoDiskQueueTest):
|
||||
chunksize = 1
|
||||
|
||||
|
|
@ -82,7 +72,7 @@ class ChunkSize4MarshalFifoDiskQueueTest(MarshalFifoDiskQueueTest):
|
|||
chunksize = 4
|
||||
|
||||
|
||||
class PickleFifoDiskQueueTest(MarshalFifoDiskQueueTest):
|
||||
class PickleFifoDiskQueueTest(t.FifoDiskQueueTest, FifoDiskQueueTestMixin):
|
||||
|
||||
chunksize = 100000
|
||||
|
||||
|
|
@ -99,12 +89,12 @@ class PickleFifoDiskQueueTest(MarshalFifoDiskQueueTest):
|
|||
|
||||
def test_serialize_loader(self):
|
||||
q = self.queue()
|
||||
l = TestLoader()
|
||||
q.push(l)
|
||||
l2 = q.pop()
|
||||
assert isinstance(l2, TestLoader)
|
||||
assert l2.default_item_class is TestItem
|
||||
self.assertEqual(l2.name_out('x'), 'xx')
|
||||
loader = TestLoader()
|
||||
q.push(loader)
|
||||
loader2 = q.pop()
|
||||
assert isinstance(loader2, TestLoader)
|
||||
assert loader2.default_item_class is TestItem
|
||||
self.assertEqual(loader2.name_out('x'), 'xx')
|
||||
|
||||
def test_serialize_request_recursive(self):
|
||||
q = self.queue()
|
||||
|
|
@ -116,6 +106,21 @@ class PickleFifoDiskQueueTest(MarshalFifoDiskQueueTest):
|
|||
self.assertEqual(r.url, r2.url)
|
||||
assert r2.meta['request'] is r2
|
||||
|
||||
def test_non_pickable_object(self):
|
||||
q = self.queue()
|
||||
try:
|
||||
q.push(lambda x: x)
|
||||
except ValueError as exc:
|
||||
if hasattr(sys, "pypy_version_info"):
|
||||
self.assertIsInstance(exc.__context__, pickle.PicklingError)
|
||||
else:
|
||||
self.assertIsInstance(exc.__context__, AttributeError)
|
||||
sel = Selector(text='<html><body><p>some text</p></body></html>')
|
||||
try:
|
||||
q.push(sel)
|
||||
except ValueError as exc:
|
||||
self.assertIsInstance(exc.__context__, TypeError)
|
||||
|
||||
|
||||
class ChunkSize1PickleFifoDiskQueueTest(PickleFifoDiskQueueTest):
|
||||
chunksize = 1
|
||||
|
|
@ -133,10 +138,7 @@ class ChunkSize4PickleFifoDiskQueueTest(PickleFifoDiskQueueTest):
|
|||
chunksize = 4
|
||||
|
||||
|
||||
class MarshalLifoDiskQueueTest(t.LifoDiskQueueTest):
|
||||
|
||||
def queue(self):
|
||||
return MarshalLifoDiskQueue(self.qpath)
|
||||
class LifoDiskQueueTestMixin:
|
||||
|
||||
def test_serialize(self):
|
||||
q = self.queue()
|
||||
|
|
@ -150,7 +152,13 @@ class MarshalLifoDiskQueueTest(t.LifoDiskQueueTest):
|
|||
test_nonserializable_object = nonserializable_object_test
|
||||
|
||||
|
||||
class PickleLifoDiskQueueTest(MarshalLifoDiskQueueTest):
|
||||
class MarshalLifoDiskQueueTest(t.LifoDiskQueueTest, LifoDiskQueueTestMixin):
|
||||
|
||||
def queue(self):
|
||||
return MarshalLifoDiskQueue(self.qpath)
|
||||
|
||||
|
||||
class PickleLifoDiskQueueTest(t.LifoDiskQueueTest, LifoDiskQueueTestMixin):
|
||||
|
||||
def queue(self):
|
||||
return PickleLifoDiskQueue(self.qpath)
|
||||
|
|
@ -165,12 +173,12 @@ class PickleLifoDiskQueueTest(MarshalLifoDiskQueueTest):
|
|||
|
||||
def test_serialize_loader(self):
|
||||
q = self.queue()
|
||||
l = TestLoader()
|
||||
q.push(l)
|
||||
l2 = q.pop()
|
||||
assert isinstance(l2, TestLoader)
|
||||
assert l2.default_item_class is TestItem
|
||||
self.assertEqual(l2.name_out('x'), 'xx')
|
||||
loader = TestLoader()
|
||||
q.push(loader)
|
||||
loader2 = q.pop()
|
||||
assert isinstance(loader2, TestLoader)
|
||||
assert loader2.default_item_class is TestItem
|
||||
self.assertEqual(loader2.name_out('x'), 'xx')
|
||||
|
||||
def test_serialize_request_recursive(self):
|
||||
q = self.queue()
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@ class BuildComponentListTest(unittest.TestCase):
|
|||
class UtilsConfTestCase(unittest.TestCase):
|
||||
|
||||
def test_arglist_to_dict(self):
|
||||
self.assertEqual(arglist_to_dict(['arg1=val1', 'arg2=val2']),
|
||||
self.assertEqual(
|
||||
arglist_to_dict(['arg1=val1', 'arg2=val2']),
|
||||
{'arg1': 'val1', 'arg2': 'val2'})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ class SequenceExcludeTest(unittest.TestCase):
|
|||
|
||||
def test_set(self):
|
||||
"""Anything that is not in the supplied sequence will evaluate as 'in' the container."""
|
||||
seq = set([-3, "test", 1.1])
|
||||
seq = {-3, "test", 1.1}
|
||||
d = SequenceExclude(seq)
|
||||
self.assertIn(0, d)
|
||||
self.assertIn("foo", d)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class DeferUtilsTest(unittest.TestCase):
|
|||
gotexc = False
|
||||
try:
|
||||
yield process_chain([cb1, cb_fail, cb3], 'res', 'v1', 'v2')
|
||||
except TypeError as e:
|
||||
except TypeError:
|
||||
gotexc = True
|
||||
self.assertTrue(gotexc)
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ class IterErrbackTest(unittest.TestCase):
|
|||
def iterbad():
|
||||
for x in range(10):
|
||||
if x == 5:
|
||||
a = 1 / 0
|
||||
1 / 0
|
||||
yield x
|
||||
|
||||
errors = []
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue