mirror of https://github.com/scrapy/scrapy.git
Deprecate mustbe_deferred() and defer_*(). (#6937)
This commit is contained in:
parent
d9ef0350d8
commit
16f168b406
|
|
@ -21,9 +21,9 @@ from scrapy.utils.asyncio import (
|
||||||
create_looping_call,
|
create_looping_call,
|
||||||
)
|
)
|
||||||
from scrapy.utils.defer import (
|
from scrapy.utils.defer import (
|
||||||
|
_defer_sleep_async,
|
||||||
deferred_from_coro,
|
deferred_from_coro,
|
||||||
maybe_deferred_to_future,
|
maybe_deferred_to_future,
|
||||||
mustbe_deferred,
|
|
||||||
)
|
)
|
||||||
from scrapy.utils.httpobj import urlparse_cached
|
from scrapy.utils.httpobj import urlparse_cached
|
||||||
|
|
||||||
|
|
@ -231,7 +231,7 @@ class Downloader:
|
||||||
try:
|
try:
|
||||||
# 1. Download the response
|
# 1. Download the response
|
||||||
response: Response = await maybe_deferred_to_future(
|
response: Response = await maybe_deferred_to_future(
|
||||||
mustbe_deferred(self.handlers.download_request, request, spider)
|
self.handlers.download_request(request, spider)
|
||||||
)
|
)
|
||||||
# 2. Notify response_downloaded listeners about the recent download
|
# 2. Notify response_downloaded listeners about the recent download
|
||||||
# before querying queue for next request
|
# before querying queue for next request
|
||||||
|
|
@ -242,6 +242,9 @@ class Downloader:
|
||||||
spider=spider,
|
spider=spider,
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
except Exception:
|
||||||
|
await _defer_sleep_async()
|
||||||
|
raise
|
||||||
finally:
|
finally:
|
||||||
# 3. After response arrives, remove the request from transferring
|
# 3. After response arrives, remove the request from transferring
|
||||||
# state to free up the transferring slot so it can be used by the
|
# state to free up the transferring slot so it can be used by the
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from scrapy.exceptions import _InvalidOutput
|
||||||
from scrapy.http import Request, Response
|
from scrapy.http import Request, Response
|
||||||
from scrapy.middleware import MiddlewareManager
|
from scrapy.middleware import MiddlewareManager
|
||||||
from scrapy.utils.conf import build_component_list
|
from scrapy.utils.conf import build_component_list
|
||||||
from scrapy.utils.defer import deferred_from_coro, mustbe_deferred
|
from scrapy.utils.defer import _defer_sleep, deferred_from_coro
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
|
@ -110,8 +110,9 @@ class DownloaderMiddlewareManager(MiddlewareManager):
|
||||||
raise exception
|
raise exception
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result: Response | Request = yield mustbe_deferred(process_request, request)
|
result: Response | Request = yield process_request(request)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
yield _defer_sleep()
|
||||||
# either returns a request or response (which we pass to process_response())
|
# either returns a request or response (which we pass to process_response())
|
||||||
# or reraises the exception
|
# or reraises the exception
|
||||||
result = yield process_exception(ex)
|
result = yield process_exception(ex)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from scrapy.exceptions import (
|
||||||
from scrapy.http import Request, Response
|
from scrapy.http import Request, Response
|
||||||
from scrapy.utils.asyncio import _parallel_asyncio, is_asyncio_available
|
from scrapy.utils.asyncio import _parallel_asyncio, is_asyncio_available
|
||||||
from scrapy.utils.defer import (
|
from scrapy.utils.defer import (
|
||||||
_defer_sleep,
|
_defer_sleep_async,
|
||||||
aiter_errback,
|
aiter_errback,
|
||||||
deferred_f_from_coro_f,
|
deferred_f_from_coro_f,
|
||||||
deferred_from_coro,
|
deferred_from_coro,
|
||||||
|
|
@ -236,7 +236,7 @@ class Scraper:
|
||||||
self, result: Response | Failure, request: Request
|
self, result: Response | Failure, request: Request
|
||||||
) -> Iterable[Any] | AsyncIterator[Any]:
|
) -> Iterable[Any] | AsyncIterator[Any]:
|
||||||
"""Call the request callback or errback with the response or failure."""
|
"""Call the request callback or errback with the response or failure."""
|
||||||
await maybe_deferred_to_future(_defer_sleep())
|
await _defer_sleep_async()
|
||||||
assert self.crawler.spider
|
assert self.crawler.spider
|
||||||
if isinstance(result, Response):
|
if isinstance(result, Response):
|
||||||
if getattr(result, "request", None) is None:
|
if getattr(result, "request", None) is None:
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,9 @@ from scrapy.middleware import MiddlewareManager
|
||||||
from scrapy.utils.asyncgen import as_async_generator, collect_asyncgen
|
from scrapy.utils.asyncgen import as_async_generator, collect_asyncgen
|
||||||
from scrapy.utils.conf import build_component_list
|
from scrapy.utils.conf import build_component_list
|
||||||
from scrapy.utils.defer import (
|
from scrapy.utils.defer import (
|
||||||
|
_defer_sleep_async,
|
||||||
deferred_from_coro,
|
deferred_from_coro,
|
||||||
maybe_deferred_to_future,
|
maybe_deferred_to_future,
|
||||||
mustbe_deferred,
|
|
||||||
)
|
)
|
||||||
from scrapy.utils.python import MutableAsyncChain, MutableChain, global_object_name
|
from scrapy.utils.python import MutableAsyncChain, MutableChain, global_object_name
|
||||||
|
|
||||||
|
|
@ -390,12 +390,11 @@ class SpiderMiddlewareManager(MiddlewareManager):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
it: Iterable[_T] | AsyncIterator[_T] = await maybe_deferred_to_future(
|
it: Iterable[_T] | AsyncIterator[_T] = await maybe_deferred_to_future(
|
||||||
mustbe_deferred(
|
self._process_spider_input(scrape_func, response, request, spider)
|
||||||
self._process_spider_input, scrape_func, response, request, spider
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
return await process_callback_output(it)
|
return await process_callback_output(it)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
await _defer_sleep_async()
|
||||||
return process_spider_exception(ex)
|
return process_spider_exception(ex)
|
||||||
|
|
||||||
async def process_start(self, spider: Spider) -> AsyncIterator[Any] | None:
|
async def process_start(self, spider: Spider) -> AsyncIterator[Any] | None:
|
||||||
|
|
|
||||||
|
|
@ -562,7 +562,7 @@ class FilesPipeline(MediaPipeline):
|
||||||
|
|
||||||
def media_to_download(
|
def media_to_download(
|
||||||
self, request: Request, info: MediaPipeline.SpiderInfo, *, item: Any = None
|
self, request: Request, info: MediaPipeline.SpiderInfo, *, item: Any = None
|
||||||
) -> Deferred[FileInfo | None]:
|
) -> Deferred[FileInfo | None] | None:
|
||||||
def _onsuccess(result: StatInfo) -> FileInfo | None:
|
def _onsuccess(result: StatInfo) -> FileInfo | None:
|
||||||
if not result:
|
if not result:
|
||||||
return None # returning None force download
|
return None # returning None force download
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,30 @@ import logging
|
||||||
import warnings
|
import warnings
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import TYPE_CHECKING, Any, Literal, NoReturn, TypedDict, Union, cast
|
from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union, cast
|
||||||
|
|
||||||
from twisted import version as twisted_version
|
from twisted import version as twisted_version
|
||||||
from twisted.internet.defer import Deferred, DeferredList
|
from twisted.internet.defer import (
|
||||||
|
Deferred,
|
||||||
|
DeferredList,
|
||||||
|
inlineCallbacks,
|
||||||
|
maybeDeferred,
|
||||||
|
)
|
||||||
from twisted.python.failure import Failure
|
from twisted.python.failure import Failure
|
||||||
from twisted.python.versions import Version
|
from twisted.python.versions import Version
|
||||||
|
|
||||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||||
from scrapy.http.request import NO_CALLBACK, Request
|
from scrapy.http.request import NO_CALLBACK, Request
|
||||||
from scrapy.settings import Settings
|
from scrapy.settings import Settings
|
||||||
|
from scrapy.utils.asyncio import call_later
|
||||||
from scrapy.utils.datatypes import SequenceExclude
|
from scrapy.utils.datatypes import SequenceExclude
|
||||||
from scrapy.utils.defer import defer_result, mustbe_deferred
|
from scrapy.utils.defer import _DEFER_DELAY, _defer_sleep
|
||||||
from scrapy.utils.log import failure_to_exc_info
|
from scrapy.utils.log import failure_to_exc_info
|
||||||
from scrapy.utils.misc import arg_to_iter
|
from scrapy.utils.misc import arg_to_iter
|
||||||
from scrapy.utils.python import get_func_args, global_object_name
|
from scrapy.utils.python import get_func_args, global_object_name
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Generator
|
||||||
|
|
||||||
# typing.Self requires Python 3.11
|
# typing.Self requires Python 3.11
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
|
|
@ -173,20 +179,25 @@ class MediaPipeline(ABC):
|
||||||
)
|
)
|
||||||
return dfd.addCallback(self.item_completed, item, info)
|
return dfd.addCallback(self.item_completed, item, info)
|
||||||
|
|
||||||
|
@inlineCallbacks
|
||||||
def _process_request(
|
def _process_request(
|
||||||
self, request: Request, info: SpiderInfo, item: Any
|
self, request: Request, info: SpiderInfo, item: Any
|
||||||
) -> Deferred[FileInfo]:
|
) -> Generator[Deferred[Any], Any, FileInfo]:
|
||||||
fp = self._fingerprinter.fingerprint(request)
|
fp = self._fingerprinter.fingerprint(request)
|
||||||
|
|
||||||
eb = request.errback
|
eb = request.errback
|
||||||
request.callback = NO_CALLBACK
|
request.callback = NO_CALLBACK
|
||||||
request.errback = None
|
request.errback = None
|
||||||
|
|
||||||
# Return cached result if request was already seen
|
# Return cached result if request was already seen
|
||||||
if fp in info.downloaded:
|
if fp in info.downloaded:
|
||||||
d = defer_result(info.downloaded[fp])
|
yield _defer_sleep()
|
||||||
if eb:
|
cached_result = info.downloaded[fp]
|
||||||
d.addErrback(eb)
|
if isinstance(cached_result, Failure):
|
||||||
return d
|
if eb:
|
||||||
|
return eb(cached_result)
|
||||||
|
cached_result.raiseException()
|
||||||
|
return cached_result
|
||||||
|
|
||||||
# Otherwise, wait for result
|
# Otherwise, wait for result
|
||||||
wad: Deferred[FileInfo] = Deferred()
|
wad: Deferred[FileInfo] = Deferred()
|
||||||
|
|
@ -196,23 +207,27 @@ class MediaPipeline(ABC):
|
||||||
|
|
||||||
# Check if request is downloading right now to avoid doing it twice
|
# Check if request is downloading right now to avoid doing it twice
|
||||||
if fp in info.downloading:
|
if fp in info.downloading:
|
||||||
return wad
|
return (yield wad)
|
||||||
|
|
||||||
# Download request checking media_to_download hook output first
|
# Download request checking media_to_download hook output first
|
||||||
info.downloading.add(fp)
|
info.downloading.add(fp)
|
||||||
dfd: Deferred[FileInfo | None] = mustbe_deferred(
|
yield _defer_sleep()
|
||||||
self.media_to_download, request, info, item=item
|
result: FileInfo | Failure
|
||||||
)
|
try:
|
||||||
dfd2: Deferred[FileInfo] = dfd.addCallback(
|
file_info = yield maybeDeferred(
|
||||||
self._check_media_to_download, request, info, item=item
|
self.media_to_download, request, info, item=item
|
||||||
)
|
)
|
||||||
dfd2.addErrback(self._log_exception)
|
if file_info:
|
||||||
dfd2.addBoth(self._cache_result_and_execute_waiters, fp, info)
|
# got a result without downloading
|
||||||
return dfd2.addBoth(lambda _: wad) # it must return wad at last
|
result = file_info
|
||||||
|
else:
|
||||||
def _log_exception(self, result: Failure) -> Failure:
|
# download the result
|
||||||
logger.exception(result)
|
result = yield self._check_media_to_download(request, info, item=item)
|
||||||
return result
|
except Exception:
|
||||||
|
result = Failure()
|
||||||
|
logger.exception(result)
|
||||||
|
self._cache_result_and_execute_waiters(result, fp, info)
|
||||||
|
return (yield wad) # it must return wad at last
|
||||||
|
|
||||||
def _modify_media_request(self, request: Request) -> None:
|
def _modify_media_request(self, request: Request) -> None:
|
||||||
if self.handle_httpstatus_list:
|
if self.handle_httpstatus_list:
|
||||||
|
|
@ -220,24 +235,29 @@ class MediaPipeline(ABC):
|
||||||
else:
|
else:
|
||||||
request.meta["handle_httpstatus_all"] = True
|
request.meta["handle_httpstatus_all"] = True
|
||||||
|
|
||||||
def _check_media_to_download(
|
@inlineCallbacks
|
||||||
self, result: FileInfo | None, request: Request, info: SpiderInfo, item: Any
|
def _check_media_to_download( # pylint: disable=inconsistent-return-statements
|
||||||
) -> FileInfo | Deferred[FileInfo]:
|
self, request: Request, info: SpiderInfo, item: Any
|
||||||
if result is not None:
|
) -> Generator[Deferred[Any], Any, FileInfo]:
|
||||||
return result
|
try:
|
||||||
dfd: Deferred[Response]
|
if self.download_func:
|
||||||
if self.download_func:
|
# this ugly code was left only to support tests. TODO: remove
|
||||||
# this ugly code was left only to support tests. TODO: remove
|
response = yield maybeDeferred(self.download_func, request, info.spider)
|
||||||
dfd = mustbe_deferred(self.download_func, request, info.spider)
|
else:
|
||||||
else:
|
self._modify_media_request(request)
|
||||||
self._modify_media_request(request)
|
assert self.crawler.engine
|
||||||
assert self.crawler.engine
|
response = yield self.crawler.engine.download(request)
|
||||||
dfd = self.crawler.engine.download(request)
|
return self.media_downloaded(response, request, info, item=item)
|
||||||
dfd2: Deferred[FileInfo] = dfd.addCallback(
|
except Exception:
|
||||||
self.media_downloaded, request, info, item=item
|
failure = self.media_failed(Failure(), request, info)
|
||||||
)
|
if isinstance(failure, Failure):
|
||||||
dfd2.addErrback(self.media_failed, request, info)
|
warnings.warn(
|
||||||
return dfd2
|
f"{global_object_name(self.media_failed)} returned a Failure instance."
|
||||||
|
f" This is deprecated, please raise an exception instead, e.g. via failure.raiseException().",
|
||||||
|
category=ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
failure.raiseException()
|
||||||
|
|
||||||
def _cache_result_and_execute_waiters(
|
def _cache_result_and_execute_waiters(
|
||||||
self, result: FileInfo | Failure, fp: bytes, info: SpiderInfo
|
self, result: FileInfo | Failure, fp: bytes, info: SpiderInfo
|
||||||
|
|
@ -273,13 +293,16 @@ class MediaPipeline(ABC):
|
||||||
info.downloading.remove(fp)
|
info.downloading.remove(fp)
|
||||||
info.downloaded[fp] = result # cache result
|
info.downloaded[fp] = result # cache result
|
||||||
for wad in info.waiting.pop(fp):
|
for wad in info.waiting.pop(fp):
|
||||||
defer_result(result).chainDeferred(wad)
|
if isinstance(result, Failure):
|
||||||
|
call_later(_DEFER_DELAY, wad.errback, result)
|
||||||
|
else:
|
||||||
|
call_later(_DEFER_DELAY, wad.callback, result)
|
||||||
|
|
||||||
# Overridable Interface
|
# Overridable Interface
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def media_to_download(
|
def media_to_download(
|
||||||
self, request: Request, info: SpiderInfo, *, item: Any = None
|
self, request: Request, info: SpiderInfo, *, item: Any = None
|
||||||
) -> Deferred[FileInfo | None]:
|
) -> Deferred[FileInfo | None] | None:
|
||||||
"""Check request before starting download"""
|
"""Check request before starting download"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
@ -303,7 +326,7 @@ class MediaPipeline(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def media_failed(
|
def media_failed(
|
||||||
self, failure: Failure, request: Request, info: SpiderInfo
|
self, failure: Failure, request: Request, info: SpiderInfo
|
||||||
) -> NoReturn:
|
) -> Failure:
|
||||||
"""Handler for failed downloads"""
|
"""Handler for failed downloads"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ from twisted.internet.defer import (
|
||||||
from twisted.internet.task import Cooperator
|
from twisted.internet.task import Cooperator
|
||||||
from twisted.python import failure
|
from twisted.python import failure
|
||||||
|
|
||||||
from scrapy.exceptions import IgnoreRequest, ScrapyDeprecationWarning
|
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||||
from scrapy.utils.asyncio import is_asyncio_available
|
from scrapy.utils.asyncio import call_later, is_asyncio_available
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import AsyncIterator, Callable
|
from collections.abc import AsyncIterator, Callable
|
||||||
|
|
@ -49,6 +49,13 @@ def defer_fail(_failure: Failure) -> Deferred[Any]:
|
||||||
It delays by 100ms so reactor has a chance to go through readers and writers
|
It delays by 100ms so reactor has a chance to go through readers and writers
|
||||||
before attending pending delayed calls, so do not set delay to zero.
|
before attending pending delayed calls, so do not set delay to zero.
|
||||||
"""
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"scrapy.utils.defer.defer_fail() is deprecated, use"
|
||||||
|
" twisted.internet.defer.fail(), plus an explicit sleep if needed.",
|
||||||
|
category=ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
d: Deferred[Any] = Deferred()
|
d: Deferred[Any] = Deferred()
|
||||||
|
|
@ -63,6 +70,13 @@ def defer_succeed(result: _T) -> Deferred[_T]:
|
||||||
It delays by 100ms so reactor has a chance to go through readers and writers
|
It delays by 100ms so reactor has a chance to go through readers and writers
|
||||||
before attending pending delayed calls, so do not set delay to zero.
|
before attending pending delayed calls, so do not set delay to zero.
|
||||||
"""
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"scrapy.utils.defer.defer_succeed() is deprecated, use"
|
||||||
|
" twisted.internet.defer.succeed(), plus an explicit sleep if needed.",
|
||||||
|
category=ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
d: Deferred[_T] = Deferred()
|
d: Deferred[_T] = Deferred()
|
||||||
|
|
@ -71,20 +85,44 @@ def defer_succeed(result: _T) -> Deferred[_T]:
|
||||||
|
|
||||||
|
|
||||||
def _defer_sleep() -> Deferred[None]:
|
def _defer_sleep() -> Deferred[None]:
|
||||||
"""Like ``defer_succeed`` and ``defer_fail`` but doesn't call any real callbacks."""
|
"""Delay by _DEFER_DELAY so reactor has a chance to go through readers and writers
|
||||||
from twisted.internet import reactor
|
before attending pending delayed calls, so do not set delay to zero.
|
||||||
|
"""
|
||||||
d: Deferred[None] = Deferred()
|
d: Deferred[None] = Deferred()
|
||||||
reactor.callLater(_DEFER_DELAY, d.callback, None)
|
call_later(_DEFER_DELAY, d.callback, None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
async def _defer_sleep_async() -> None:
|
||||||
|
"""Delay by _DEFER_DELAY so reactor has a chance to go through readers and writers
|
||||||
|
before attending pending delayed calls, so do not set delay to zero.
|
||||||
|
"""
|
||||||
|
if is_asyncio_available():
|
||||||
|
await asyncio.sleep(_DEFER_DELAY)
|
||||||
|
else:
|
||||||
|
await _defer_sleep()
|
||||||
|
|
||||||
|
|
||||||
def defer_result(result: Any) -> Deferred[Any]:
|
def defer_result(result: Any) -> Deferred[Any]:
|
||||||
|
warnings.warn(
|
||||||
|
"scrapy.utils.defer.defer_result() is deprecated, use"
|
||||||
|
" twisted.internet.defer.success() and twisted.internet.defer.fail(),"
|
||||||
|
" plus an explicit sleep if needed, or explicit reactor.callLater().",
|
||||||
|
category=ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
|
||||||
if isinstance(result, Deferred):
|
if isinstance(result, Deferred):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
from twisted.internet import reactor
|
||||||
|
|
||||||
|
d: Deferred[Any] = Deferred()
|
||||||
if isinstance(result, failure.Failure):
|
if isinstance(result, failure.Failure):
|
||||||
return defer_fail(result)
|
reactor.callLater(_DEFER_DELAY, d.errback, result)
|
||||||
return defer_succeed(result)
|
else:
|
||||||
|
reactor.callLater(_DEFER_DELAY, d.callback, result)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
|
@ -93,14 +131,6 @@ def mustbe_deferred(
|
||||||
) -> Deferred[_T]: ...
|
) -> Deferred[_T]: ...
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def mustbe_deferred(
|
|
||||||
f: Callable[_P, Coroutine[Deferred[Any], Any, _T]],
|
|
||||||
*args: _P.args,
|
|
||||||
**kw: _P.kwargs,
|
|
||||||
) -> Deferred[_T]: ...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def mustbe_deferred(
|
def mustbe_deferred(
|
||||||
f: Callable[_P, _T], *args: _P.args, **kw: _P.kwargs
|
f: Callable[_P, _T], *args: _P.args, **kw: _P.kwargs
|
||||||
|
|
@ -108,22 +138,24 @@ def mustbe_deferred(
|
||||||
|
|
||||||
|
|
||||||
def mustbe_deferred(
|
def mustbe_deferred(
|
||||||
f: Callable[_P, Deferred[_T] | Coroutine[Deferred[Any], Any, _T] | _T],
|
f: Callable[_P, Deferred[_T] | _T],
|
||||||
*args: _P.args,
|
*args: _P.args,
|
||||||
**kw: _P.kwargs,
|
**kw: _P.kwargs,
|
||||||
) -> Deferred[_T]:
|
) -> Deferred[_T]:
|
||||||
"""Same as twisted.internet.defer.maybeDeferred, but delay calling
|
"""Same as twisted.internet.defer.maybeDeferred, but delay calling
|
||||||
callback/errback to next reactor loop
|
callback/errback to next reactor loop
|
||||||
"""
|
"""
|
||||||
|
warnings.warn(
|
||||||
|
"scrapy.utils.defer.mustbe_deferred() is deprecated, use"
|
||||||
|
" twisted.internet.defer.maybeDeferred(), with an explicit sleep if needed.",
|
||||||
|
category=ScrapyDeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
result: _T | Deferred[_T] | Failure
|
||||||
try:
|
try:
|
||||||
result = f(*args, **kw)
|
result = f(*args, **kw)
|
||||||
# FIXME: Hack to avoid introspecting tracebacks. This to speed up
|
|
||||||
# processing of IgnoreRequest errors which are, by far, the most common
|
|
||||||
# exception in Scrapy - see #125
|
|
||||||
except IgnoreRequest as e:
|
|
||||||
return defer_fail(failure.Failure(e))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return defer_fail(failure.Failure())
|
result = failure.Failure()
|
||||||
return defer_result(result)
|
return defer_result(result)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from twisted.python.failure import Failure
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
|
|
||||||
from scrapy import signals
|
from scrapy import signals
|
||||||
|
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||||
from scrapy.http import Request, Response
|
from scrapy.http import Request, Response
|
||||||
from scrapy.http.request import NO_CALLBACK
|
from scrapy.http.request import NO_CALLBACK
|
||||||
from scrapy.pipelines.files import FileException
|
from scrapy.pipelines.files import FileException
|
||||||
|
|
@ -36,7 +37,7 @@ class UserDefinedPipeline(MediaPipeline):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def media_failed(self, failure, request, info):
|
def media_failed(self, failure, request, info):
|
||||||
return failure
|
failure.raiseException()
|
||||||
|
|
||||||
def file_path(self, request, response=None, info=None, *, item=None):
|
def file_path(self, request, response=None, info=None, *, item=None):
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -227,7 +228,8 @@ class TestMediaPipeline(TestBaseMediaPipeline):
|
||||||
@inlineCallbacks
|
@inlineCallbacks
|
||||||
def test_result_failure(self):
|
def test_result_failure(self):
|
||||||
self.pipe.LOG_FAILED_RESULTS = False
|
self.pipe.LOG_FAILED_RESULTS = False
|
||||||
fail = Failure(Exception())
|
exc = Exception("foo")
|
||||||
|
fail = Failure(exc)
|
||||||
req = Request(
|
req = Request(
|
||||||
"http://url1",
|
"http://url1",
|
||||||
meta={"response": fail},
|
meta={"response": fail},
|
||||||
|
|
@ -235,7 +237,10 @@ class TestMediaPipeline(TestBaseMediaPipeline):
|
||||||
)
|
)
|
||||||
item = {"requests": req}
|
item = {"requests": req}
|
||||||
new_item = yield self.pipe.process_item(item, self.spider)
|
new_item = yield self.pipe.process_item(item, self.spider)
|
||||||
assert new_item["results"] == [(False, fail)]
|
assert len(new_item["results"]) == 1
|
||||||
|
assert new_item["results"][0][0] is False
|
||||||
|
assert isinstance(new_item["results"][0][1], Failure)
|
||||||
|
assert new_item["results"][0][1].value == exc
|
||||||
assert self.pipe._mockcalled == [
|
assert self.pipe._mockcalled == [
|
||||||
"get_media_requests",
|
"get_media_requests",
|
||||||
"media_to_download",
|
"media_to_download",
|
||||||
|
|
@ -249,11 +254,16 @@ class TestMediaPipeline(TestBaseMediaPipeline):
|
||||||
self.pipe.LOG_FAILED_RESULTS = False
|
self.pipe.LOG_FAILED_RESULTS = False
|
||||||
rsp1 = Response("http://url1")
|
rsp1 = Response("http://url1")
|
||||||
req1 = Request("http://url1", meta={"response": rsp1})
|
req1 = Request("http://url1", meta={"response": rsp1})
|
||||||
fail = Failure(Exception())
|
exc = Exception("foo")
|
||||||
|
fail = Failure(exc)
|
||||||
req2 = Request("http://url2", meta={"response": fail})
|
req2 = Request("http://url2", meta={"response": fail})
|
||||||
item = {"requests": [req1, req2]}
|
item = {"requests": [req1, req2]}
|
||||||
new_item = yield self.pipe.process_item(item, self.spider)
|
new_item = yield self.pipe.process_item(item, self.spider)
|
||||||
assert new_item["results"] == [(True, {}), (False, fail)]
|
assert len(new_item["results"]) == 2
|
||||||
|
assert new_item["results"][0] == (True, {})
|
||||||
|
assert new_item["results"][1][0] is False
|
||||||
|
assert isinstance(new_item["results"][1][1], Failure)
|
||||||
|
assert new_item["results"][1][1].value == exc
|
||||||
m = self.pipe._mockcalled
|
m = self.pipe._mockcalled
|
||||||
# only once
|
# only once
|
||||||
assert m[0] == "get_media_requests" # first hook called
|
assert m[0] == "get_media_requests" # first hook called
|
||||||
|
|
@ -532,3 +542,62 @@ class TestBuildFromCrawler:
|
||||||
assert pipe._fingerprinter
|
assert pipe._fingerprinter
|
||||||
assert len(w) == 0
|
assert len(w) == 0
|
||||||
assert pipe._from_crawler_called
|
assert pipe._from_crawler_called
|
||||||
|
|
||||||
|
|
||||||
|
class MediaFailedFailurePipeline(MockedMediaPipeline):
|
||||||
|
def media_failed(self, failure, request, info):
|
||||||
|
self._mockcalled.append("media_failed")
|
||||||
|
return failure # deprecated
|
||||||
|
|
||||||
|
|
||||||
|
class TestMediaFailedFailure(unittest.TestCase):
|
||||||
|
"""Test that media_failed() can return a failure instead of raising."""
|
||||||
|
|
||||||
|
pipeline_class = MediaFailedFailurePipeline
|
||||||
|
settings = None
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
spider_cls = Spider
|
||||||
|
self.spider = spider_cls("media.com")
|
||||||
|
crawler = get_crawler(spider_cls, self.settings)
|
||||||
|
self.pipe = self.pipeline_class.from_crawler(crawler)
|
||||||
|
self.pipe.download_func = _mocked_download_func
|
||||||
|
self.pipe.open_spider(self.spider)
|
||||||
|
self.info = self.pipe.spiderinfo
|
||||||
|
self.fingerprint = crawler.request_fingerprinter.fingerprint
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
for name, signal in vars(signals).items():
|
||||||
|
if not name.startswith("_"):
|
||||||
|
disconnect_all(signal)
|
||||||
|
|
||||||
|
def _errback(self, result):
|
||||||
|
self.pipe._mockcalled.append("request_errback")
|
||||||
|
return result
|
||||||
|
|
||||||
|
@inlineCallbacks
|
||||||
|
def test_result_failure(self):
|
||||||
|
self.pipe.LOG_FAILED_RESULTS = False
|
||||||
|
exc = Exception("foo")
|
||||||
|
fail = Failure(exc)
|
||||||
|
req = Request(
|
||||||
|
"http://url1",
|
||||||
|
meta={"response": fail},
|
||||||
|
errback=self._errback,
|
||||||
|
)
|
||||||
|
item = {"requests": req}
|
||||||
|
with pytest.warns(
|
||||||
|
ScrapyDeprecationWarning, match="media_failed returned a Failure instance"
|
||||||
|
):
|
||||||
|
new_item = yield self.pipe.process_item(item, self.spider)
|
||||||
|
assert len(new_item["results"]) == 1
|
||||||
|
assert new_item["results"][0][0] is False
|
||||||
|
assert isinstance(new_item["results"][0][1], Failure)
|
||||||
|
assert new_item["results"][0][1].value == exc
|
||||||
|
assert self.pipe._mockcalled == [
|
||||||
|
"get_media_requests",
|
||||||
|
"media_to_download",
|
||||||
|
"media_failed",
|
||||||
|
"request_errback",
|
||||||
|
"item_completed",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import AsyncIterator, Iterable
|
from collections.abc import AsyncIterator, Iterable
|
||||||
from inspect import isasyncgen
|
from inspect import isasyncgen
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -18,6 +18,9 @@ from scrapy.utils.asyncgen import collect_asyncgen
|
||||||
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future
|
from scrapy.utils.defer import deferred_f_from_coro_f, maybe_deferred_to_future
|
||||||
from scrapy.utils.test import get_crawler
|
from scrapy.utils.test import get_crawler
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from twisted.python.failure import Failure
|
||||||
|
|
||||||
|
|
||||||
class TestSpiderMiddleware(TestCase):
|
class TestSpiderMiddleware(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
@ -31,7 +34,13 @@ class TestSpiderMiddleware(TestCase):
|
||||||
"""Execute spider mw manager's scrape_response method and return the result.
|
"""Execute spider mw manager's scrape_response method and return the result.
|
||||||
Raise exception in case of failure.
|
Raise exception in case of failure.
|
||||||
"""
|
"""
|
||||||
scrape_func = mock.MagicMock()
|
|
||||||
|
def scrape_func(
|
||||||
|
response: Response | Failure, request: Request
|
||||||
|
) -> defer.Deferred[Iterable[Any]]:
|
||||||
|
it = mock.MagicMock()
|
||||||
|
return defer.succeed(it)
|
||||||
|
|
||||||
return await maybe_deferred_to_future(
|
return await maybe_deferred_to_future(
|
||||||
self.mwman.scrape_response(
|
self.mwman.scrape_response(
|
||||||
scrape_func, self.response, self.request, self.spider
|
scrape_func, self.response, self.request, self.spider
|
||||||
|
|
@ -122,11 +131,16 @@ class TestBaseAsyncSpiderMiddleware(TestSpiderMiddleware):
|
||||||
start_index = 10
|
start_index = 10
|
||||||
return {i: c for c, i in enumerate(mw_classes, start=start_index)}
|
return {i: c for c, i in enumerate(mw_classes, start=start_index)}
|
||||||
|
|
||||||
def _scrape_func(self, *args, **kwargs):
|
def _callback(self) -> Any:
|
||||||
yield {"foo": 1}
|
yield {"foo": 1}
|
||||||
yield {"foo": 2}
|
yield {"foo": 2}
|
||||||
yield {"foo": 3}
|
yield {"foo": 3}
|
||||||
|
|
||||||
|
def _scrape_func(
|
||||||
|
self, response: Response | Failure, request: Request
|
||||||
|
) -> defer.Deferred[Iterable[Any] | AsyncIterator[Any]]:
|
||||||
|
return defer.succeed(self._callback())
|
||||||
|
|
||||||
async def _get_middleware_result(
|
async def _get_middleware_result(
|
||||||
self, *mw_classes: type[Any], start_index: int | None = None
|
self, *mw_classes: type[Any], start_index: int | None = None
|
||||||
) -> Any:
|
) -> Any:
|
||||||
|
|
@ -272,8 +286,8 @@ class TestProcessSpiderOutputSimple(TestBaseAsyncSpiderMiddleware):
|
||||||
class TestProcessSpiderOutputAsyncGen(TestProcessSpiderOutputSimple):
|
class TestProcessSpiderOutputAsyncGen(TestProcessSpiderOutputSimple):
|
||||||
"""process_spider_output tests for async generator callbacks"""
|
"""process_spider_output tests for async generator callbacks"""
|
||||||
|
|
||||||
async def _scrape_func(self, *args, **kwargs):
|
async def _callback(self) -> Any:
|
||||||
for item in super()._scrape_func():
|
for item in super()._callback():
|
||||||
yield item
|
yield item
|
||||||
|
|
||||||
@deferred_f_from_coro_f
|
@deferred_f_from_coro_f
|
||||||
|
|
@ -503,8 +517,8 @@ class TestBuiltinMiddlewareSimple(TestBaseAsyncSpiderMiddleware):
|
||||||
|
|
||||||
|
|
||||||
class TestBuiltinMiddlewareAsyncGen(TestBuiltinMiddlewareSimple):
|
class TestBuiltinMiddlewareAsyncGen(TestBuiltinMiddlewareSimple):
|
||||||
async def _scrape_func(self, *args, **kwargs):
|
async def _callback(self) -> Any:
|
||||||
for item in super()._scrape_func():
|
for item in super()._callback():
|
||||||
yield item
|
yield item
|
||||||
|
|
||||||
@deferred_f_from_coro_f
|
@deferred_f_from_coro_f
|
||||||
|
|
@ -546,7 +560,7 @@ class TestProcessSpiderException(TestBaseAsyncSpiderMiddleware):
|
||||||
MW_EXC_SIMPLE = ProcessSpiderExceptionSimpleIterableMiddleware
|
MW_EXC_SIMPLE = ProcessSpiderExceptionSimpleIterableMiddleware
|
||||||
MW_EXC_ASYNCGEN = ProcessSpiderExceptionAsyncIteratorMiddleware
|
MW_EXC_ASYNCGEN = ProcessSpiderExceptionAsyncIteratorMiddleware
|
||||||
|
|
||||||
def _scrape_func(self, *args, **kwargs):
|
def _callback(self) -> Any:
|
||||||
1 / 0
|
1 / 0
|
||||||
|
|
||||||
async def _test_asyncgen_nodowngrade(self, *mw_classes: type[Any]) -> None:
|
async def _test_asyncgen_nodowngrade(self, *mw_classes: type[Any]) -> None:
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ if TYPE_CHECKING:
|
||||||
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
|
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.filterwarnings("ignore::scrapy.exceptions.ScrapyDeprecationWarning")
|
||||||
class TestMustbeDeferred(unittest.TestCase):
|
class TestMustbeDeferred(unittest.TestCase):
|
||||||
@inlineCallbacks
|
@inlineCallbacks
|
||||||
def test_success_function(self) -> Generator[Deferred[Any], Any, None]:
|
def test_success_function(self) -> Generator[Deferred[Any], Any, None]:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue