scrapy/scrapy/utils/spider.py

131 lines
3.5 KiB
Python

from __future__ import annotations
import inspect
import logging
from typing import TYPE_CHECKING, Any, TypeVar, overload
from scrapy.spiders import Spider
from scrapy.utils.defer import deferred_from_coro
from scrapy.utils.misc import arg_to_iter
if TYPE_CHECKING:
from collections.abc import AsyncGenerator, Iterable
from types import CoroutineType, ModuleType
from twisted.internet.defer import Deferred
from scrapy import Request
from scrapy.spiderloader import SpiderLoaderProtocol
logger = logging.getLogger(__name__)
_T = TypeVar("_T")
# https://stackoverflow.com/questions/60222982
@overload
def iterate_spider_output(result: AsyncGenerator[_T]) -> AsyncGenerator[_T]: ... # type: ignore[overload-overlap]
@overload
def iterate_spider_output(result: CoroutineType[Any, Any, _T]) -> Deferred[_T]: ...
@overload
def iterate_spider_output(result: _T) -> Iterable[Any]: ...
def iterate_spider_output(
result: Any,
) -> Iterable[Any] | AsyncGenerator[_T] | Deferred[_T]:
if inspect.isasyncgen(result):
return result
d: Deferred[_T] = deferred_from_coro(result)
if inspect.iscoroutine(result):
return d.addCallback(iterate_spider_output)
return arg_to_iter(d)
def iter_spider_classes(module: ModuleType) -> Iterable[type[Spider]]:
"""Return an iterator over all spider classes defined in the given module
that can be instantiated (i.e. which have name)
"""
for obj in vars(module).values():
if (
inspect.isclass(obj)
and issubclass(obj, Spider)
and obj.__module__ == module.__name__
and getattr(obj, "name", None)
):
yield obj
@overload
def spidercls_for_request(
spider_loader: SpiderLoaderProtocol,
request: Request,
default_spidercls: type[Spider],
log_none: bool = ...,
log_multiple: bool = ...,
) -> type[Spider]: ...
@overload
def spidercls_for_request(
spider_loader: SpiderLoaderProtocol,
request: Request,
default_spidercls: None,
log_none: bool = ...,
log_multiple: bool = ...,
) -> type[Spider] | None: ...
@overload
def spidercls_for_request(
spider_loader: SpiderLoaderProtocol,
request: Request,
*,
log_none: bool = ...,
log_multiple: bool = ...,
) -> type[Spider] | None: ...
def spidercls_for_request(
spider_loader: SpiderLoaderProtocol,
request: Request,
default_spidercls: type[Spider] | None = None,
log_none: bool = False,
log_multiple: bool = False,
) -> type[Spider] | None:
"""Return a spider class that handles the given Request.
This will look for the spiders that can handle the given request (using
the spider loader) and return a Spider class if (and only if) there is
only one Spider able to handle the Request.
If multiple spiders (or no spider) are found, it will return the
default_spidercls passed. It can optionally log if multiple or no spiders
are found.
"""
snames = spider_loader.find_by_request(request)
if len(snames) == 1:
return spider_loader.load(snames[0])
if len(snames) > 1 and log_multiple:
logger.error(
"More than one spider can handle: %(request)s - %(snames)s",
{"request": request, "snames": ", ".join(snames)},
)
if len(snames) == 0 and log_none:
logger.error(
"Unable to find spider that handles: %(request)s", {"request": request}
)
return default_spidercls
class DefaultSpider(Spider):
name = "default"