Merge branch 'master' into 4307-use-f-strings

Conflicts resolved:
- delete tests/py36/_test_crawl.py
This commit is contained in:
Ammar Najjar 2020-08-29 07:12:44 +02:00
commit defeaacbc2
36 changed files with 176 additions and 264 deletions

View File

@ -40,7 +40,7 @@ including a list of features.
Requirements Requirements
============ ============
* Python 3.5.2+ * Python 3.6+
* Works on Linux, Windows, macOS, BSD * Works on Linux, Windows, macOS, BSD
Install Install

View File

@ -14,8 +14,6 @@ collect_ignore = [
*_py_files("tests/CrawlerProcess"), *_py_files("tests/CrawlerProcess"),
# contains scripts to be run by tests/test_crawler.py::CrawlerRunnerSubprocess # contains scripts to be run by tests/test_crawler.py::CrawlerRunnerSubprocess
*_py_files("tests/CrawlerRunner"), *_py_files("tests/CrawlerRunner"),
# Py36-only parts of respective tests
*_py_files("tests/py36"),
] ]
for line in open('tests/ignores.txt'): for line in open('tests/ignores.txt'):

View File

@ -199,6 +199,17 @@ In any case, if something is covered in a docstring, use the
documentation instead of duplicating the docstring in files within the documentation instead of duplicating the docstring in files within the
``docs/`` directory. ``docs/`` directory.
Documentation updates that cover new or modified features must use Sphinxs
:rst:dir:`versionadded` and :rst:dir:`versionchanged` directives. Use
``VERSION`` as version, we will replace it with the actual version right before
the corresponding release. When we release a new major or minor version of
Scrapy, we remove these directives if they are older than 3 years.
Documentation about deprecated features must be removed as those features are
deprecated, so that new readers do not run into it. New deprecations and
deprecation removals are documented in the :ref:`release notes <news>`.
Tests Tests
===== =====

View File

@ -9,8 +9,8 @@ Installation guide
Supported Python versions Supported Python versions
========================= =========================
Scrapy requires Python 3.5.2+, either the CPython implementation (default) or Scrapy requires Python 3.6+, either the CPython implementation (default) or
the PyPy 5.9+ implementation (see :ref:`python:implementations`). the PyPy 7.2.0+ implementation (see :ref:`python:implementations`).
Installing Scrapy Installing Scrapy

View File

@ -405,8 +405,6 @@ to get all of them:
from sys import version_info from sys import version_info
.. skip: next if(version_info < (3, 6), reason="Only Python 3.6+ dictionaries match the output")
Having figured out how to extract each bit, we can now iterate over all the Having figured out how to extract each bit, we can now iterate over all the
quotes elements and put them together into a Python dictionary: quotes elements and put them together into a Python dictionary:

View File

@ -4,8 +4,6 @@
Core API Core API
======== ========
.. versionadded:: 0.15
This section documents the Scrapy core API, and it's intended for developers of This section documents the Scrapy core API, and it's intended for developers of
extensions and middlewares. extensions and middlewares.

View File

@ -128,8 +128,6 @@ The maximum download delay (in seconds) to be set in case of high latencies.
AUTOTHROTTLE_TARGET_CONCURRENCY AUTOTHROTTLE_TARGET_CONCURRENCY
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 1.1
Default: ``1.0`` Default: ``1.0``
Average number of requests Scrapy should be sending in parallel to remote Average number of requests Scrapy should be sending in parallel to remote

View File

@ -4,8 +4,6 @@
Benchmarking Benchmarking
============ ============
.. versionadded:: 0.17
Scrapy comes with a simple benchmarking suite that spawns a local HTTP server Scrapy comes with a simple benchmarking suite that spawns a local HTTP server
and crawls it at the maximum possible speed. The goal of this benchmarking is and crawls it at the maximum possible speed. The goal of this benchmarking is
to get an idea of how Scrapy performs in your hardware, in order to have a to get an idea of how Scrapy performs in your hardware, in order to have a

View File

@ -6,8 +6,6 @@
Command line tool Command line tool
================= =================
.. versionadded:: 0.10
Scrapy is controlled through the ``scrapy`` command-line tool, to be referred Scrapy is controlled through the ``scrapy`` command-line tool, to be referred
here as the "Scrapy tool" to differentiate it from the sub-commands, which we here as the "Scrapy tool" to differentiate it from the sub-commands, which we
just call "commands" or "Scrapy commands". just call "commands" or "Scrapy commands".
@ -566,8 +564,6 @@ and Platform info, which is useful for bug reports.
bench bench
----- -----
.. versionadded:: 0.17
* Syntax: ``scrapy bench`` * Syntax: ``scrapy bench``
* Requires project: *no* * Requires project: *no*

View File

@ -4,8 +4,6 @@
Spiders Contracts Spiders Contracts
================= =================
.. versionadded:: 0.15
Testing spiders can get particularly annoying and while nothing prevents you Testing spiders can get particularly annoying and while nothing prevents you
from writing unit tests the task gets cumbersome quickly. Scrapy offers an from writing unit tests the task gets cumbersome quickly. Scrapy offers an
integrated way of testing your spiders by the means of contracts. integrated way of testing your spiders by the means of contracts.

View File

@ -17,19 +17,14 @@ hence use coroutine syntax (e.g. ``await``, ``async for``, ``async with``):
- :class:`~scrapy.http.Request` callbacks. - :class:`~scrapy.http.Request` callbacks.
The following are known caveats of the current implementation that we aim .. note:: The callback output is not processed until the whole callback
to address in future versions of Scrapy: finishes.
- The callback output is not processed until the whole callback finishes.
As a side effect, if the callback raises an exception, none of its As a side effect, if the callback raises an exception, none of its
output is processed. output is processed.
- Because `asynchronous generators were introduced in Python 3.6`_, you This is a known caveat of the current implementation that we aim to
can only use ``yield`` if you are using Python 3.6 or later. address in a future version of Scrapy.
If you need to output multiple items or requests and you are using
Python 3.5, return an iterable (e.g. a list) instead.
- The :meth:`process_item` method of - The :meth:`process_item` method of
:ref:`item pipelines <topics-item-pipeline>`. :ref:`item pipelines <topics-item-pipeline>`.
@ -44,8 +39,6 @@ hence use coroutine syntax (e.g. ``await``, ``async for``, ``async with``):
- :ref:`Signal handlers that support deferreds <signal-deferred>`. - :ref:`Signal handlers that support deferreds <signal-deferred>`.
.. _asynchronous generators were introduced in Python 3.6: https://www.python.org/dev/peps/pep-0525/
Usage Usage
===== =====

View File

@ -217,8 +217,6 @@ The following settings can be used to configure the cookie middleware:
Multiple cookie sessions per spider Multiple cookie sessions per spider
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 0.15
There is support for keeping multiple cookie sessions per spider by using the There is support for keeping multiple cookie sessions per spider by using the
:reqmeta:`cookiejar` Request meta key. By default it uses a single cookie jar :reqmeta:`cookiejar` Request meta key. By default it uses a single cookie jar
(session), but you can pass an identifier to use different ones. (session), but you can pass an identifier to use different ones.
@ -475,8 +473,6 @@ DBM storage backend
.. class:: DbmCacheStorage .. class:: DbmCacheStorage
.. versionadded:: 0.13
A DBM_ storage backend is also available for the HTTP cache middleware. A DBM_ storage backend is also available for the HTTP cache middleware.
By default, it uses the :mod:`dbm`, but you can change it with the By default, it uses the :mod:`dbm`, but you can change it with the
@ -549,15 +545,10 @@ settings:
HTTPCACHE_ENABLED HTTPCACHE_ENABLED
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
.. versionadded:: 0.11
Default: ``False`` Default: ``False``
Whether the HTTP cache will be enabled. Whether the HTTP cache will be enabled.
.. versionchanged:: 0.11
Before 0.11, :setting:`HTTPCACHE_DIR` was used to enable cache.
.. setting:: HTTPCACHE_EXPIRATION_SECS .. setting:: HTTPCACHE_EXPIRATION_SECS
HTTPCACHE_EXPIRATION_SECS HTTPCACHE_EXPIRATION_SECS
@ -570,9 +561,6 @@ Expiration time for cached requests, in seconds.
Cached requests older than this time will be re-downloaded. If zero, cached Cached requests older than this time will be re-downloaded. If zero, cached
requests will never expire. requests will never expire.
.. versionchanged:: 0.11
Before 0.11, zero meant cached requests always expire.
.. setting:: HTTPCACHE_DIR .. setting:: HTTPCACHE_DIR
HTTPCACHE_DIR HTTPCACHE_DIR
@ -589,8 +577,6 @@ project data dir. For more info see: :ref:`topics-project-structure`.
HTTPCACHE_IGNORE_HTTP_CODES HTTPCACHE_IGNORE_HTTP_CODES
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 0.10
Default: ``[]`` Default: ``[]``
Don't cache response with these HTTP codes. Don't cache response with these HTTP codes.
@ -609,8 +595,6 @@ If enabled, requests not found in the cache will be ignored instead of downloade
HTTPCACHE_IGNORE_SCHEMES HTTPCACHE_IGNORE_SCHEMES
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 0.10
Default: ``['file']`` Default: ``['file']``
Don't cache responses with these URI schemes. Don't cache responses with these URI schemes.
@ -629,8 +613,6 @@ The class which implements the cache storage backend.
HTTPCACHE_DBM_MODULE HTTPCACHE_DBM_MODULE
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 0.13
Default: ``'dbm'`` Default: ``'dbm'``
The database module to use in the :ref:`DBM storage backend The database module to use in the :ref:`DBM storage backend
@ -641,8 +623,6 @@ The database module to use in the :ref:`DBM storage backend
HTTPCACHE_POLICY HTTPCACHE_POLICY
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
.. versionadded:: 0.18
Default: ``'scrapy.extensions.httpcache.DummyPolicy'`` Default: ``'scrapy.extensions.httpcache.DummyPolicy'``
The class which implements the cache policy. The class which implements the cache policy.
@ -652,8 +632,6 @@ The class which implements the cache policy.
HTTPCACHE_GZIP HTTPCACHE_GZIP
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
.. versionadded:: 1.0
Default: ``False`` Default: ``False``
If enabled, will compress all cached data with gzip. If enabled, will compress all cached data with gzip.
@ -664,8 +642,6 @@ This setting is specific to the Filesystem backend.
HTTPCACHE_ALWAYS_STORE HTTPCACHE_ALWAYS_STORE
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 1.1
Default: ``False`` Default: ``False``
If enabled, will cache pages unconditionally. If enabled, will cache pages unconditionally.
@ -684,8 +660,6 @@ responses you feed to the cache middleware.
HTTPCACHE_IGNORE_RESPONSE_CACHE_CONTROLS HTTPCACHE_IGNORE_RESPONSE_CACHE_CONTROLS
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 1.1
Default: ``[]`` Default: ``[]``
List of Cache-Control directives in responses to be ignored. List of Cache-Control directives in responses to be ignored.
@ -735,8 +709,6 @@ HttpProxyMiddleware
.. module:: scrapy.downloadermiddlewares.httpproxy .. module:: scrapy.downloadermiddlewares.httpproxy
:synopsis: Http Proxy Middleware :synopsis: Http Proxy Middleware
.. versionadded:: 0.8
.. reqmeta:: proxy .. reqmeta:: proxy
.. class:: HttpProxyMiddleware .. class:: HttpProxyMiddleware
@ -817,8 +789,6 @@ RedirectMiddleware settings
REDIRECT_ENABLED REDIRECT_ENABLED
^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
.. versionadded:: 0.13
Default: ``True`` Default: ``True``
Whether the Redirect middleware will be enabled. Whether the Redirect middleware will be enabled.
@ -860,8 +830,6 @@ MetaRefreshMiddleware settings
METAREFRESH_ENABLED METAREFRESH_ENABLED
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
.. versionadded:: 0.17
Default: ``True`` Default: ``True``
Whether the Meta Refresh middleware will be enabled. Whether the Meta Refresh middleware will be enabled.
@ -924,8 +892,6 @@ RetryMiddleware Settings
RETRY_ENABLED RETRY_ENABLED
^^^^^^^^^^^^^ ^^^^^^^^^^^^^
.. versionadded:: 0.13
Default: ``True`` Default: ``True``
Whether the Retry middleware will be enabled. Whether the Retry middleware will be enabled.
@ -1179,8 +1145,6 @@ AjaxCrawlMiddleware Settings
AJAXCRAWL_ENABLED AJAXCRAWL_ENABLED
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
.. versionadded:: 0.21
Default: ``False`` Default: ``False``
Whether the AjaxCrawlMiddleware will be enabled. You may want to Whether the AjaxCrawlMiddleware will be enabled. You may want to

View File

@ -292,7 +292,7 @@ XmlItemExporter
CsvItemExporter CsvItemExporter
--------------- ---------------
.. class:: CsvItemExporter(file, include_headers_line=True, join_multivalued=',', **kwargs) .. class:: CsvItemExporter(file, include_headers_line=True, join_multivalued=',', errors=None, **kwargs)
Exports items in CSV format to the given file-like object. If the Exports items in CSV format to the given file-like object. If the
:attr:`fields_to_export` attribute is set, it will be used to define the :attr:`fields_to_export` attribute is set, it will be used to define the
@ -311,6 +311,11 @@ CsvItemExporter
multi-valued fields, if found. multi-valued fields, if found.
:type include_headers_line: str :type include_headers_line: str
:param errors: The optional string that specifies how encoding and decoding
errors are to be handled. For more information see
:class:`io.TextIOWrapper`.
:type errors: str
The additional keyword arguments of this ``__init__`` method are passed to the The additional keyword arguments of this ``__init__`` method are passed to the
:class:`BaseItemExporter` ``__init__`` method, and the leftover arguments to the :class:`BaseItemExporter` ``__init__`` method, and the leftover arguments to the
:func:`csv.writer` function, so you can use any :func:`csv.writer` function :func:`csv.writer` function, so you can use any :func:`csv.writer` function

View File

@ -288,8 +288,6 @@ If zero (or non set), spiders won't be closed by number of passed items.
CLOSESPIDER_PAGECOUNT CLOSESPIDER_PAGECOUNT
""""""""""""""""""""" """""""""""""""""""""
.. versionadded:: 0.11
Default: ``0`` Default: ``0``
An integer which specifies the maximum number of responses to crawl. If the spider An integer which specifies the maximum number of responses to crawl. If the spider
@ -302,8 +300,6 @@ number of crawled responses.
CLOSESPIDER_ERRORCOUNT CLOSESPIDER_ERRORCOUNT
"""""""""""""""""""""" """"""""""""""""""""""
.. versionadded:: 0.11
Default: ``0`` Default: ``0``
An integer which specifies the maximum number of errors to receive before An integer which specifies the maximum number of errors to receive before

View File

@ -4,8 +4,6 @@
Feed exports Feed exports
============ ============
.. versionadded:: 0.10
One of the most frequently required features when implementing scrapers is One of the most frequently required features when implementing scrapers is
being able to store the scraped data properly and, quite often, that means being able to store the scraped data properly and, quite often, that means
generating an "export file" with the scraped data (commonly called "export generating an "export file" with the scraped data (commonly called "export

View File

@ -553,18 +553,6 @@ fields with form data from :class:`Response` objects.
The other parameters of this class method are passed directly to the The other parameters of this class method are passed directly to the
:class:`FormRequest` ``__init__`` method. :class:`FormRequest` ``__init__`` method.
.. versionadded:: 0.10.3
The ``formname`` parameter.
.. versionadded:: 0.17
The ``formxpath`` parameter.
.. versionadded:: 1.1.0
The ``formcss`` parameter.
.. versionadded:: 1.1.0
The ``formid`` parameter.
Request usage examples Request usage examples
---------------------- ----------------------

View File

@ -1076,8 +1076,6 @@ See :ref:`topics-extensions-ref-memusage`.
MEMUSAGE_CHECK_INTERVAL_SECONDS MEMUSAGE_CHECK_INTERVAL_SECONDS
------------------------------- -------------------------------
.. versionadded:: 1.1
Default: ``60.0`` Default: ``60.0``
Scope: ``scrapy.extensions.memusage`` Scope: ``scrapy.extensions.memusage``
@ -1358,8 +1356,6 @@ The class that will be used for loading spiders, which must implement the
SPIDER_LOADER_WARN_ONLY SPIDER_LOADER_WARN_ONLY
----------------------- -----------------------
.. versionadded:: 1.3.3
Default: ``False`` Default: ``False``
By default, when Scrapy tries to import spider classes from :setting:`SPIDER_MODULES`, By default, when Scrapy tries to import spider classes from :setting:`SPIDER_MODULES`,

View File

@ -146,8 +146,6 @@ object gives you access, for example, to the :ref:`settings <topics-settings>`.
.. method:: process_start_requests(start_requests, spider) .. method:: process_start_requests(start_requests, spider)
.. versionadded:: 0.15
This method is called with the start requests of the spider, and works This method is called with the start requests of the spider, and works
similarly to the :meth:`process_spider_output` method, except that it similarly to the :meth:`process_spider_output` method, except that it
doesn't have a response associated and must return only requests (not doesn't have a response associated and must return only requests (not
@ -341,8 +339,6 @@ RefererMiddleware settings
REFERER_ENABLED REFERER_ENABLED
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
.. versionadded:: 0.15
Default: ``True`` Default: ``True``
Whether to enable referer middleware. Whether to enable referer middleware.
@ -352,8 +348,6 @@ Whether to enable referer middleware.
REFERRER_POLICY REFERRER_POLICY
^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
.. versionadded:: 1.4
Default: ``'scrapy.spidermiddlewares.referer.DefaultReferrerPolicy'`` Default: ``'scrapy.spidermiddlewares.referer.DefaultReferrerPolicy'``
.. reqmeta:: referrer_policy .. reqmeta:: referrer_policy

View File

@ -28,8 +28,8 @@ twisted_version = (_txv.major, _txv.minor, _txv.micro)
# Check minimum required Python version # Check minimum required Python version
if sys.version_info < (3, 5, 2): if sys.version_info < (3, 6):
print("Scrapy %s requires Python 3.5.2" % __version__) print("Scrapy %s requires Python 3.6+" % __version__)
sys.exit(1) sys.exit(1)

View File

@ -195,7 +195,7 @@ class XmlItemExporter(BaseItemExporter):
class CsvItemExporter(BaseItemExporter): class CsvItemExporter(BaseItemExporter):
def __init__(self, file, include_headers_line=True, join_multivalued=',', **kwargs): def __init__(self, file, include_headers_line=True, join_multivalued=',', errors=None, **kwargs):
super().__init__(dont_fail=True, **kwargs) super().__init__(dont_fail=True, **kwargs)
if not self.encoding: if not self.encoding:
self.encoding = 'utf-8' self.encoding = 'utf-8'
@ -205,7 +205,8 @@ class CsvItemExporter(BaseItemExporter):
line_buffering=False, line_buffering=False,
write_through=True, write_through=True,
encoding=self.encoding, encoding=self.encoding,
newline='' # Windows needs this https://github.com/scrapy/scrapy/issues/3034 newline='', # Windows needs this https://github.com/scrapy/scrapy/issues/3034
errors=errors,
) )
self.csv_writer = csv.writer(self.stream, **self._kwargs) self.csv_writer = csv.writer(self.stream, **self._kwargs)
self._headers_not_written = True self._headers_not_written = True

View File

@ -5,6 +5,7 @@ See documentation in docs/topics/spiders.rst
""" """
import logging import logging
import warnings import warnings
from typing import Optional
from scrapy import signals from scrapy import signals
from scrapy.http import Request from scrapy.http import Request
@ -18,8 +19,8 @@ class Spider(object_ref):
class. class.
""" """
name = None name: Optional[str] = None
custom_settings = None custom_settings: Optional[dict] = None
def __init__(self, name=None, **kwargs): def __init__(self, name=None, **kwargs):
if name is not None: if name is not None:

View File

@ -7,6 +7,7 @@ See documentation in docs/topics/spiders.rst
import copy import copy
import warnings import warnings
from typing import Sequence
from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.http import Request, HtmlResponse from scrapy.http import Request, HtmlResponse
@ -72,7 +73,7 @@ class Rule:
class CrawlSpider(Spider): class CrawlSpider(Spider):
rules = () rules: Sequence[Rule] = ()
def __init__(self, *a, **kw): def __init__(self, *a, **kw):
super().__init__(*a, **kw) super().__init__(*a, **kw)

View File

@ -124,18 +124,11 @@ def iter_errback(iterable, errback, *a, **kw):
errback(failure.Failure(), *a, **kw) errback(failure.Failure(), *a, **kw)
def _isfuture(o):
# workaround for Python before 3.5.3 not having asyncio.isfuture
if hasattr(asyncio, 'isfuture'):
return asyncio.isfuture(o)
return isinstance(o, asyncio.Future)
def deferred_from_coro(o): def deferred_from_coro(o):
"""Converts a coroutine into a Deferred, or returns the object as is if it isn't a coroutine""" """Converts a coroutine into a Deferred, or returns the object as is if it isn't a coroutine"""
if isinstance(o, defer.Deferred): if isinstance(o, defer.Deferred):
return o return o
if _isfuture(o) or inspect.isawaitable(o): if asyncio.isfuture(o) or inspect.isawaitable(o):
if not is_asyncio_reactor_installed(): if not is_asyncio_reactor_installed():
# wrapping the coroutine directly into a Deferred, this doesn't work correctly with coroutines # wrapping the coroutine directly into a Deferred, this doesn't work correctly with coroutines
# that use asyncio, e.g. "await asyncio.sleep(1)" # that use asyncio, e.g. "await asyncio.sleep(1)"
@ -167,7 +160,7 @@ def maybeDeferred_coro(f, *args, **kw):
if isinstance(result, defer.Deferred): if isinstance(result, defer.Deferred):
return result return result
elif _isfuture(result) or inspect.isawaitable(result): elif asyncio.isfuture(result) or inspect.isawaitable(result):
return deferred_from_coro(result) return deferred_from_coro(result)
elif isinstance(result, failure.Failure): elif isinstance(result, failure.Failure):
return defer.fail(result) return defer.fail(result)

View File

@ -6,11 +6,10 @@ import struct
from scrapy.utils.decorators import deprecated from scrapy.utils.decorators import deprecated
# - Python>=3.5 GzipFile's read() has issues returning leftover # - GzipFile's read() has issues returning leftover uncompressed data when
# uncompressed data when input is corrupted # input is corrupted
# (regression or bug-fix compared to Python 3.4)
# - read1(), which fetches data before raising EOFError on next call # - read1(), which fetches data before raising EOFError on next call
# works here but is only available from Python>=3.3 # works here
@deprecated('GzipFile.read1') @deprecated('GzipFile.read1')
def read1(gzf, size=-1): def read1(gzf, size=-1):
return gzf.read1(size) return gzf.read1(size)

View File

@ -12,10 +12,12 @@ def render_templatefile(path, **kwargs):
content = string.Template(raw).substitute(**kwargs) content = string.Template(raw).substitute(**kwargs)
render_path = path[:-len('.tmpl')] if path.endswith('.tmpl') else path render_path = path[:-len('.tmpl')] if path.endswith('.tmpl') else path
if path.endswith('.tmpl'):
os.rename(path, render_path)
with open(render_path, 'wb') as fp: with open(render_path, 'wb') as fp:
fp.write(content.encode('utf8')) fp.write(content.encode('utf8'))
if path.endswith('.tmpl'):
os.remove(path)
CAMELCASE_INVALID_CHARS = re.compile(r'[^a-zA-Z\d]') CAMELCASE_INVALID_CHARS = re.compile(r'[^a-zA-Z\d]')

View File

@ -16,9 +16,6 @@ ignore_errors = True
[mypy-scrapy.commands] [mypy-scrapy.commands]
ignore_errors = True ignore_errors = True
[mypy-scrapy.commands.bench]
ignore_errors = True
[mypy-scrapy.commands.parse] [mypy-scrapy.commands.parse]
ignore_errors = True ignore_errors = True
@ -28,9 +25,6 @@ ignore_errors = True
[mypy-scrapy.contracts] [mypy-scrapy.contracts]
ignore_errors = True ignore_errors = True
[mypy-scrapy.core.spidermw]
ignore_errors = True
[mypy-scrapy.interfaces] [mypy-scrapy.interfaces]
ignore_errors = True ignore_errors = True
@ -70,15 +64,6 @@ ignore_errors = True
[mypy-tests.mocks.dummydbm] [mypy-tests.mocks.dummydbm]
ignore_errors = True ignore_errors = True
[mypy-tests.spiders]
ignore_errors = True
[mypy-tests.test_cmdline_crawl_with_pipeline.test_spider.spiders.exception]
ignore_errors = True
[mypy-tests.test_cmdline_crawl_with_pipeline.test_spider.spiders.normal]
ignore_errors = True
[mypy-tests.test_command_fetch] [mypy-tests.test_command_fetch]
ignore_errors = True ignore_errors = True
@ -94,9 +79,6 @@ ignore_errors = True
[mypy-tests.test_contracts] [mypy-tests.test_contracts]
ignore_errors = True ignore_errors = True
[mypy-tests.test_crawler]
ignore_errors = True
[mypy-tests.test_downloader_handlers] [mypy-tests.test_downloader_handlers]
ignore_errors = True ignore_errors = True
@ -127,53 +109,20 @@ ignore_errors = True
[mypy-tests.test_pipeline_images] [mypy-tests.test_pipeline_images]
ignore_errors = True ignore_errors = True
[mypy-tests.test_pipelines]
ignore_errors = True
[mypy-tests.test_request_attribute_binding]
ignore_errors = True
[mypy-tests.test_request_cb_kwargs] [mypy-tests.test_request_cb_kwargs]
ignore_errors = True ignore_errors = True
[mypy-tests.test_request_left]
ignore_errors = True
[mypy-tests.test_scheduler] [mypy-tests.test_scheduler]
ignore_errors = True ignore_errors = True
[mypy-tests.test_signals]
ignore_errors = True
[mypy-tests.test_spiderloader.test_spiders.nested.spider4]
ignore_errors = True
[mypy-tests.test_spiderloader.test_spiders.spider1]
ignore_errors = True
[mypy-tests.test_spiderloader.test_spiders.spider2]
ignore_errors = True
[mypy-tests.test_spiderloader.test_spiders.spider3]
ignore_errors = True
[mypy-tests.test_spidermiddleware_httperror] [mypy-tests.test_spidermiddleware_httperror]
ignore_errors = True ignore_errors = True
[mypy-tests.test_spidermiddleware_output_chain]
ignore_errors = True
[mypy-tests.test_spidermiddleware_referer] [mypy-tests.test_spidermiddleware_referer]
ignore_errors = True ignore_errors = True
[mypy-tests.test_utils_reqser]
ignore_errors = True
[mypy-tests.test_utils_serialize] [mypy-tests.test_utils_serialize]
ignore_errors = True ignore_errors = True
[mypy-tests.test_utils_spider]
ignore_errors = True
[mypy-tests.test_utils_url] [mypy-tests.test_utils_url]
ignore_errors = True ignore_errors = True

View File

@ -82,7 +82,6 @@ setup(
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
@ -92,7 +91,7 @@ setup(
'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Software Development :: Libraries :: Application Frameworks',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
], ],
python_requires='>=3.5.2', python_requires='>=3.6',
install_requires=install_requires, install_requires=install_requires,
extras_require=extras_require, extras_require=extras_require,
) )

View File

@ -1,57 +0,0 @@
import asyncio
from scrapy import Request
from tests.spiders import SimpleSpider
class AsyncDefAsyncioGenSpider(SimpleSpider):
name = 'asyncdef_asyncio_gen'
async def parse(self, response):
await asyncio.sleep(0.2)
yield {'foo': 42}
self.logger.info("Got response %d" % response.status)
class AsyncDefAsyncioGenLoopSpider(SimpleSpider):
name = 'asyncdef_asyncio_gen_loop'
async def parse(self, response):
for i in range(10):
await asyncio.sleep(0.1)
yield {'foo': i}
self.logger.info("Got response %d" % response.status)
class AsyncDefAsyncioGenComplexSpider(SimpleSpider):
name = 'asyncdef_asyncio_gen_complex'
initial_reqs = 4
following_reqs = 3
depth = 2
def _get_req(self, index, cb=None):
return Request(self.mockserver.url(f"/status?n=200&request={index}"),
meta={'index': index},
dont_filter=True,
callback=cb)
def start_requests(self):
for i in range(1, self.initial_reqs + 1):
yield self._get_req(i)
async def parse(self, response):
index = response.meta['index']
yield {'index': index}
if index < 10 ** self.depth:
for new_index in range(10 * index, 10 * index + self.following_reqs):
yield self._get_req(new_index)
yield self._get_req(index, cb=self.parse2)
await asyncio.sleep(0.1)
yield {'index': index + 5}
async def parse2(self, response):
await asyncio.sleep(0.1)
yield {'index2': response.meta['index']}

View File

@ -2,8 +2,7 @@
attrs attrs
dataclasses; python_version == '3.6' dataclasses; python_version == '3.6'
mitmproxy; python_version >= '3.7' mitmproxy; python_version >= '3.7'
mitmproxy >= 4, < 5; python_version >= '3.6' and python_version < '3.7' mitmproxy >= 4.0.4, < 5; python_version >= '3.6' and python_version < '3.7'
mitmproxy < 4; python_version < '3.6'
pyftpdlib pyftpdlib
# https://github.com/pytest-dev/pytest-twisted/issues/93 # https://github.com/pytest-dev/pytest-twisted/issues/93
pytest != 5.4, != 5.4.1 pytest != 5.4, != 5.4.1

View File

@ -148,6 +148,59 @@ class AsyncDefAsyncioReqsReturnSpider(SimpleSpider):
return reqs return reqs
class AsyncDefAsyncioGenSpider(SimpleSpider):
name = 'asyncdef_asyncio_gen'
async def parse(self, response):
await asyncio.sleep(0.2)
yield {'foo': 42}
self.logger.info("Got response %d" % response.status)
class AsyncDefAsyncioGenLoopSpider(SimpleSpider):
name = 'asyncdef_asyncio_gen_loop'
async def parse(self, response):
for i in range(10):
await asyncio.sleep(0.1)
yield {'foo': i}
self.logger.info("Got response %d" % response.status)
class AsyncDefAsyncioGenComplexSpider(SimpleSpider):
name = 'asyncdef_asyncio_gen_complex'
initial_reqs = 4
following_reqs = 3
depth = 2
def _get_req(self, index, cb=None):
return Request(self.mockserver.url("/status?n=200&request=%d" % index),
meta={'index': index},
dont_filter=True,
callback=cb)
def start_requests(self):
for i in range(1, self.initial_reqs + 1):
yield self._get_req(i)
async def parse(self, response):
index = response.meta['index']
yield {'index': index}
if index < 10 ** self.depth:
for new_index in range(10 * index, 10 * index + self.following_reqs):
yield self._get_req(new_index)
yield self._get_req(index, cb=self.parse2)
await asyncio.sleep(0.1)
yield {'index': index + 5}
async def parse2(self, response):
await asyncio.sleep(0.1)
yield {'index2': response.meta['index']}
class ItemSpider(FollowAllSpider): class ItemSpider(FollowAllSpider):
name = 'item' name = 'item'
@ -255,7 +308,7 @@ class CrawlSpiderWithParseMethod(MockServerSpider, CrawlSpider):
A CrawlSpider which overrides the 'parse' method A CrawlSpider which overrides the 'parse' method
""" """
name = 'crawl_spider_with_parse_method' name = 'crawl_spider_with_parse_method'
custom_settings = { custom_settings: dict = {
'RETRY_HTTP_CODES': [], # no need to retry 'RETRY_HTTP_CODES': [], # no need to retry
} }
rules = ( rules = (

View File

@ -128,9 +128,13 @@ class StartprojectTest(ProjectTest):
def get_permissions_dict(path, renamings=None, ignore=None): def get_permissions_dict(path, renamings=None, ignore=None):
def get_permissions(path):
return oct(os.stat(path).st_mode)
renamings = renamings or tuple() renamings = renamings or tuple()
permissions_dict = { permissions_dict = {
'.': os.stat(path).st_mode, '.': get_permissions(path),
} }
for root, dirs, files in os.walk(path): for root, dirs, files in os.walk(path):
nodes = list(chain(dirs, files)) nodes = list(chain(dirs, files))
@ -145,13 +149,15 @@ def get_permissions_dict(path, renamings=None, ignore=None):
search_string, search_string,
replacement replacement
) )
permissions = os.stat(absolute_path).st_mode permissions = get_permissions(absolute_path)
permissions_dict[relative_path] = permissions permissions_dict[relative_path] = permissions
return permissions_dict return permissions_dict
class StartprojectTemplatesTest(ProjectTest): class StartprojectTemplatesTest(ProjectTest):
maxDiff = None
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.tmpl = join(self.temp_path, 'templates') self.tmpl = join(self.temp_path, 'templates')
@ -293,7 +299,7 @@ class StartprojectTemplatesTest(ProjectTest):
path.mkdir(mode=permissions) path.mkdir(mode=permissions)
else: else:
path.touch(mode=permissions) path.touch(mode=permissions)
expected_permissions[node] = path.stat().st_mode expected_permissions[node] = oct(path.stat().st_mode)
process = subprocess.Popen( process = subprocess.Popen(
( (
@ -313,6 +319,53 @@ class StartprojectTemplatesTest(ProjectTest):
self.assertEqual(actual_permissions, expected_permissions) self.assertEqual(actual_permissions, expected_permissions)
def test_startproject_permissions_umask_022(self):
"""Check that generated files have the right permissions when the
system uses a umask value that causes new files to have different
permissions than those from the template folder."""
@contextmanager
def umask(new_mask):
cur_mask = os.umask(new_mask)
yield
os.umask(cur_mask)
scrapy_path = scrapy.__path__[0]
project_template = os.path.join(
scrapy_path,
'templates',
'project'
)
project_name = 'umaskproject'
renamings = (
('module', project_name),
('.tmpl', ''),
)
expected_permissions = get_permissions_dict(
project_template,
renamings,
IGNORE,
)
with umask(0o002):
destination = mkdtemp()
process = subprocess.Popen(
(
sys.executable,
'-m',
'scrapy.cmdline',
'startproject',
project_name,
),
cwd=destination,
env=self.env,
)
process.wait()
project_dir = os.path.join(destination, project_name)
actual_permissions = get_permissions_dict(project_dir)
self.assertEqual(actual_permissions, expected_permissions)
class CommandTest(ProjectTest): class CommandTest(ProjectTest):

View File

@ -1,6 +1,5 @@
import json import json
import logging import logging
import sys
from ipaddress import IPv4Address from ipaddress import IPv4Address
from socket import gethostbyname from socket import gethostbyname
from urllib.parse import urlparse from urllib.parse import urlparse
@ -20,6 +19,9 @@ from scrapy.http.response import Response
from scrapy.utils.python import to_unicode from scrapy.utils.python import to_unicode
from tests.mockserver import MockServer from tests.mockserver import MockServer
from tests.spiders import ( from tests.spiders import (
AsyncDefAsyncioGenComplexSpider,
AsyncDefAsyncioGenLoopSpider,
AsyncDefAsyncioGenSpider,
AsyncDefAsyncioReqsReturnSpider, AsyncDefAsyncioReqsReturnSpider,
AsyncDefAsyncioReturnSingleElementSpider, AsyncDefAsyncioReturnSingleElementSpider,
AsyncDefAsyncioReturnSpider, AsyncDefAsyncioReturnSpider,
@ -405,11 +407,9 @@ class CrawlSpiderTestCase(TestCase):
self.assertIn("Got response 200", str(log)) self.assertIn("Got response 200", str(log))
self.assertIn({"foo": 42}, items) self.assertIn({"foo": 42}, items)
@mark.skipif(sys.version_info < (3, 6), reason="Async generators require Python 3.6 or higher")
@mark.only_asyncio() @mark.only_asyncio()
@defer.inlineCallbacks @defer.inlineCallbacks
def test_async_def_asyncgen_parse(self): def test_async_def_asyncgen_parse(self):
from tests.py36._test_crawl import AsyncDefAsyncioGenSpider
crawler = self.runner.create_crawler(AsyncDefAsyncioGenSpider) crawler = self.runner.create_crawler(AsyncDefAsyncioGenSpider)
with LogCapture() as log: with LogCapture() as log:
yield crawler.crawl(self.mockserver.url("/status?n=200"), mockserver=self.mockserver) yield crawler.crawl(self.mockserver.url("/status?n=200"), mockserver=self.mockserver)
@ -417,7 +417,6 @@ class CrawlSpiderTestCase(TestCase):
itemcount = crawler.stats.get_value('item_scraped_count') itemcount = crawler.stats.get_value('item_scraped_count')
self.assertEqual(itemcount, 1) self.assertEqual(itemcount, 1)
@mark.skipif(sys.version_info < (3, 6), reason="Async generators require Python 3.6 or higher")
@mark.only_asyncio() @mark.only_asyncio()
@defer.inlineCallbacks @defer.inlineCallbacks
def test_async_def_asyncgen_parse_loop(self): def test_async_def_asyncgen_parse_loop(self):
@ -426,7 +425,6 @@ class CrawlSpiderTestCase(TestCase):
def _on_item_scraped(item): def _on_item_scraped(item):
items.append(item) items.append(item)
from tests.py36._test_crawl import AsyncDefAsyncioGenLoopSpider
crawler = self.runner.create_crawler(AsyncDefAsyncioGenLoopSpider) crawler = self.runner.create_crawler(AsyncDefAsyncioGenLoopSpider)
crawler.signals.connect(_on_item_scraped, signals.item_scraped) crawler.signals.connect(_on_item_scraped, signals.item_scraped)
with LogCapture() as log: with LogCapture() as log:
@ -437,7 +435,6 @@ class CrawlSpiderTestCase(TestCase):
for i in range(10): for i in range(10):
self.assertIn({'foo': i}, items) self.assertIn({'foo': i}, items)
@mark.skipif(sys.version_info < (3, 6), reason="Async generators require Python 3.6 or higher")
@mark.only_asyncio() @mark.only_asyncio()
@defer.inlineCallbacks @defer.inlineCallbacks
def test_async_def_asyncgen_parse_complex(self): def test_async_def_asyncgen_parse_complex(self):
@ -446,7 +443,6 @@ class CrawlSpiderTestCase(TestCase):
def _on_item_scraped(item): def _on_item_scraped(item):
items.append(item) items.append(item)
from tests.py36._test_crawl import AsyncDefAsyncioGenComplexSpider
crawler = self.runner.create_crawler(AsyncDefAsyncioGenComplexSpider) crawler = self.runner.create_crawler(AsyncDefAsyncioGenComplexSpider)
crawler.signals.connect(_on_item_scraped, signals.item_scraped) crawler.signals.connect(_on_item_scraped, signals.item_scraped)
yield crawler.crawl(mockserver=self.mockserver) yield crawler.crawl(mockserver=self.mockserver)

View File

@ -355,6 +355,23 @@ class CsvItemExporterTest(BaseItemExporterTest):
expected='22,False,3.14,2015-01-01 01:01:01\r\n' expected='22,False,3.14,2015-01-01 01:01:01\r\n'
) )
def test_errors_default(self):
with self.assertRaises(UnicodeEncodeError):
self.assertExportResult(
item=dict(text=u'W\u0275\u200Brd'),
expected=None,
encoding='windows-1251',
)
def test_errors_xmlcharrefreplace(self):
self.assertExportResult(
item=dict(text=u'W\u0275\u200Brd'),
include_headers_line=False,
expected='W&#629;&#8203;rd\r\n',
encoding='windows-1251',
errors='xmlcharrefreplace',
)
class CsvItemExporterDataclassTest(CsvItemExporterTest): class CsvItemExporterDataclassTest(CsvItemExporterTest):
item_class = TestDataClass item_class = TestDataClass

View File

@ -1,4 +1,3 @@
import sys
import unittest import unittest
from unittest import mock from unittest import mock
from warnings import catch_warnings from warnings import catch_warnings
@ -7,9 +6,6 @@ from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.item import ABCMeta, _BaseItem, BaseItem, 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)
class ItemTest(unittest.TestCase): class ItemTest(unittest.TestCase):
def assertSortedEqual(self, first, second, msg=None): def assertSortedEqual(self, first, second, msg=None):
@ -280,14 +276,6 @@ class ItemMetaTest(unittest.TestCase):
with mock.patch.object(base, '__new__', new_mock): with mock.patch.object(base, '__new__', new_mock):
class MyItem(Item): class MyItem(Item):
if not PY36_PLUS:
# This attribute is an internal attribute in Python 3.6+
# and must be propagated properly. See
# https://docs.python.org/3.6/reference/datamodel.html#creating-the-class-object
# In <3.6, we add a dummy attribute just to ensure the
# __new__ method propagates it correctly.
__classcell__ = object()
def f(self): def f(self):
# For rationale of this see: # For rationale of this see:
# https://github.com/python/cpython/blob/ee1a81b77444c6715cbe610e951c655b6adab88b/Lib/test/test_super.py#L222 # https://github.com/python/cpython/blob/ee1a81b77444c6715cbe610e951c655b6adab88b/Lib/test/test_super.py#L222

View File

@ -7,7 +7,6 @@ from subprocess import Popen, PIPE
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit
from unittest import skipIf from unittest import skipIf
import pytest
from testfixtures import LogCapture from testfixtures import LogCapture
from twisted.internet import defer from twisted.internet import defer
from twisted.trial.unittest import TestCase from twisted.trial.unittest import TestCase
@ -58,8 +57,6 @@ def _wrong_credentials(proxy_url):
return urlunsplit(bad_auth_proxy) return urlunsplit(bad_auth_proxy)
@skipIf(sys.version_info < (3, 5, 4),
"requires mitmproxy < 3.0.0, which these tests do not support")
@skipIf("pypy" in sys.executable, @skipIf("pypy" in sys.executable,
"mitmproxy does not support PyPy") "mitmproxy does not support PyPy")
@skipIf(platform.system() == 'Windows' and sys.version_info < (3, 7), @skipIf(platform.system() == 'Windows' and sys.version_info < (3, 7),
@ -88,14 +85,6 @@ class ProxyConnectTestCase(TestCase):
yield crawler.crawl(self.mockserver.url("/status?n=200", is_secure=True)) yield crawler.crawl(self.mockserver.url("/status?n=200", is_secure=True))
self._assert_got_response_code(200, log) self._assert_got_response_code(200, log)
@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 log:
yield crawler.crawl("https://localhost:99999/status?n=200")
self._assert_got_tunnel_error(log)
@defer.inlineCallbacks @defer.inlineCallbacks
def test_https_tunnel_auth_error(self): def test_https_tunnel_auth_error(self):
os.environ['https_proxy'] = _wrong_credentials(os.environ['https_proxy']) os.environ['https_proxy'] = _wrong_credentials(os.environ['https_proxy'])

View File

@ -89,7 +89,7 @@ deps =
basepython = python3 basepython = python3
deps = deps =
{[pinned]deps} {[pinned]deps}
# First lxml version that includes a Windows wheel for Python 3.5, so we do # First lxml version that includes a Windows wheel for Python 3.6, so we do
# not need to build lxml from sources in a CI Windows job: # not need to build lxml from sources in a CI Windows job:
lxml==3.8.0 lxml==3.8.0