diff --git a/scrapy/core/scheduler.py b/scrapy/core/scheduler.py index 3fb0bbaff..70b6dc8a1 100644 --- a/scrapy/core/scheduler.py +++ b/scrapy/core/scheduler.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import json import logging from abc import abstractmethod from pathlib import Path -from typing import Any, Optional, Type, TypeVar, cast +from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, cast from twisted.internet.defer import Deferred @@ -14,6 +16,11 @@ from scrapy.statscollectors import StatsCollector from scrapy.utils.job import job_dir from scrapy.utils.misc import create_instance, load_object +if TYPE_CHECKING: + # typing.Self requires Python 3.11 + from typing_extensions import Self + + logger = logging.getLogger(__name__) @@ -54,7 +61,7 @@ class BaseScheduler(metaclass=BaseSchedulerMeta): """ @classmethod - def from_crawler(cls, crawler: Crawler): + def from_crawler(cls, crawler: Crawler) -> Self: """ Factory method which receives the current :class:`~scrapy.crawler.Crawler` object as argument. """ @@ -325,6 +332,7 @@ class Scheduler(BaseScheduler): def _dq(self): """Create a new priority queue instance, with disk storage""" + assert self.dqdir state = self._read_dqs_state(self.dqdir) q = create_instance( self.pqclass, diff --git a/scrapy/core/scraper.py b/scrapy/core/scraper.py index a85f6a661..a54929712 100644 --- a/scrapy/core/scraper.py +++ b/scrapy/core/scraper.py @@ -364,6 +364,7 @@ class Scraper: spider=spider, exception=output.value, ) + assert ex logkws = self.logformatter.item_error(item, ex, response, spider) logger.log( *logformatter_adapter(logkws), diff --git a/scrapy/crawler.py b/scrapy/crawler.py index bc0ab02df..25823b6ac 100644 --- a/scrapy/crawler.py +++ b/scrapy/crawler.py @@ -4,9 +4,14 @@ import logging import pprint import signal import warnings -from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Type, Union +from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Set, Type, Union, cast -from twisted.internet import defer +from twisted.internet.defer import ( + Deferred, + DeferredList, + inlineCallbacks, + maybeDeferred, +) from zope.interface.exceptions import DoesNotImplement try: @@ -24,9 +29,8 @@ from scrapy.exceptions import ScrapyDeprecationWarning from scrapy.extension import ExtensionManager from scrapy.interfaces import ISpiderLoader from scrapy.logformatter import LogFormatter -from scrapy.settings import Settings, overridden_settings +from scrapy.settings import BaseSettings, Settings, overridden_settings from scrapy.signalmanager import SignalManager -from scrapy.spiderloader import SpiderLoader from scrapy.statscollectors import StatsCollector from scrapy.utils.log import ( LogCounterHandler, @@ -123,8 +127,8 @@ class Crawler: self.spider: Optional[Spider] = None self.engine: Optional[ExecutionEngine] = None - @defer.inlineCallbacks - def crawl(self, *args, **kwargs): + @inlineCallbacks + def crawl(self, *args: Any, **kwargs: Any) -> Generator[Deferred, Any, None]: if self.crawling: raise RuntimeError("Crawling already taking place") self.crawling = True @@ -134,26 +138,27 @@ class Crawler: self.engine = self._create_engine() start_requests = iter(self.spider.start_requests()) yield self.engine.open_spider(self.spider, start_requests) - yield defer.maybeDeferred(self.engine.start) + yield maybeDeferred(self.engine.start) except Exception: self.crawling = False if self.engine is not None: yield self.engine.close() raise - def _create_spider(self, *args, **kwargs): + def _create_spider(self, *args: Any, **kwargs: Any) -> Spider: return self.spidercls.from_crawler(self, *args, **kwargs) - def _create_engine(self): + def _create_engine(self) -> ExecutionEngine: return ExecutionEngine(self, lambda _: self.stop()) - @defer.inlineCallbacks - def stop(self): + @inlineCallbacks + def stop(self) -> Generator[Deferred, Any, None]: """Starts a graceful stop of the crawler and returns a deferred that is fired when the crawler is stopped.""" if self.crawling: self.crawling = False - yield defer.maybeDeferred(self.engine.stop) + assert self.engine + yield maybeDeferred(self.engine.stop) class CrawlerRunner: @@ -176,7 +181,7 @@ class CrawlerRunner: ) @staticmethod - def _get_spider_loader(settings) -> SpiderLoader: + def _get_spider_loader(settings: BaseSettings): """Get SpiderLoader instance from settings""" cls_path = settings.get("SPIDER_LOADER_CLASS") loader_cls = load_object(cls_path) @@ -201,10 +206,15 @@ class CrawlerRunner: self.settings = settings self.spider_loader = self._get_spider_loader(settings) self._crawlers: Set[Crawler] = set() - self._active: Set[defer.Deferred] = set() + self._active: Set[Deferred] = set() self.bootstrap_failed = False - def crawl(self, crawler_or_spidercls, *args, **kwargs): + def crawl( + self, + crawler_or_spidercls: Union[Type[Spider], str, Crawler], + *args: Any, + **kwargs: Any, + ) -> Deferred: """ Run a crawler with the provided arguments. @@ -234,12 +244,12 @@ class CrawlerRunner: crawler = self.create_crawler(crawler_or_spidercls) return self._crawl(crawler, *args, **kwargs) - def _crawl(self, crawler, *args, **kwargs): + def _crawl(self, crawler: Crawler, *args: Any, **kwargs: Any) -> Deferred: self.crawlers.add(crawler) d = crawler.crawl(*args, **kwargs) self._active.add(d) - def _done(result): + def _done(result: Any) -> Any: self.crawlers.discard(crawler) self._active.discard(d) self.bootstrap_failed |= not getattr(crawler, "spider", None) @@ -272,18 +282,19 @@ class CrawlerRunner: def _create_crawler(self, spidercls: Union[str, Type[Spider]]) -> Crawler: if isinstance(spidercls, str): spidercls = self.spider_loader.load(spidercls) - return Crawler(spidercls, self.settings) + # temporary cast until self.spider_loader is typed + return Crawler(cast(Type[Spider], spidercls), self.settings) - def stop(self): + def stop(self) -> Deferred: """ Stops simultaneously all the crawling jobs taking place. Returns a deferred that is fired when they all have ended. """ - return defer.DeferredList([c.stop() for c in list(self.crawlers)]) + return DeferredList([c.stop() for c in list(self.crawlers)]) - @defer.inlineCallbacks - def join(self): + @inlineCallbacks + def join(self) -> Generator[Deferred, Any, None]: """ join() @@ -291,7 +302,7 @@ class CrawlerRunner: completed their executions. """ while self._active: - yield defer.DeferredList(self._active) + yield DeferredList(self._active) class CrawlerProcess(CrawlerRunner): @@ -318,13 +329,17 @@ class CrawlerProcess(CrawlerRunner): process. See :ref:`run-from-script` for an example. """ - def __init__(self, settings=None, install_root_handler=True): + def __init__( + self, + settings: Union[Dict[str, Any], Settings, None] = None, + install_root_handler: bool = True, + ): super().__init__(settings) configure_logging(self.settings, install_root_handler) log_scrapy_info(self.settings) self._initialized_reactor = False - def _signal_shutdown(self, signum, _): + def _signal_shutdown(self, signum: int, _: Any) -> None: from twisted.internet import reactor install_shutdown_handlers(self._signal_kill) @@ -335,7 +350,7 @@ class CrawlerProcess(CrawlerRunner): ) reactor.callFromThread(self._graceful_stop_reactor) - def _signal_kill(self, signum, _): + def _signal_kill(self, signum: int, _: Any) -> None: from twisted.internet import reactor install_shutdown_handlers(signal.SIG_IGN) @@ -345,14 +360,19 @@ class CrawlerProcess(CrawlerRunner): ) reactor.callFromThread(self._stop_reactor) - def _create_crawler(self, spidercls): + def _create_crawler(self, spidercls: Union[Type[Spider], str]) -> Crawler: if isinstance(spidercls, str): spidercls = self.spider_loader.load(spidercls) init_reactor = not self._initialized_reactor self._initialized_reactor = True - return Crawler(spidercls, self.settings, init_reactor=init_reactor) + # temporary cast until self.spider_loader is typed + return Crawler( + cast(Type[Spider], spidercls), self.settings, init_reactor=init_reactor + ) - def start(self, stop_after_crawl=True, install_signal_handlers=True): + def start( + self, stop_after_crawl: bool = True, install_signal_handlers: bool = True + ) -> None: """ This method starts a :mod:`~twisted.internet.reactor`, adjusts its pool size to :setting:`REACTOR_THREADPOOL_MAXSIZE`, and installs a DNS cache @@ -386,12 +406,12 @@ class CrawlerProcess(CrawlerRunner): reactor.addSystemEventTrigger("before", "shutdown", self.stop) reactor.run(installSignalHandlers=False) # blocking call - def _graceful_stop_reactor(self): + def _graceful_stop_reactor(self) -> Deferred: d = self.stop() d.addBoth(self._stop_reactor) return d - def _stop_reactor(self, _=None): + def _stop_reactor(self, _: Any = None) -> None: from twisted.internet import reactor try: diff --git a/scrapy/dupefilters.py b/scrapy/dupefilters.py index d796e5cbb..d2639104b 100644 --- a/scrapy/dupefilters.py +++ b/scrapy/dupefilters.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import logging from pathlib import Path -from typing import Optional, Set, Type, TypeVar +from typing import TYPE_CHECKING, Optional, Set from warnings import warn from twisted.internet.defer import Deferred @@ -10,16 +12,22 @@ from scrapy.settings import BaseSettings from scrapy.spiders import Spider from scrapy.utils.deprecate import ScrapyDeprecationWarning from scrapy.utils.job import job_dir -from scrapy.utils.request import RequestFingerprinter, referer_str +from scrapy.utils.request import ( + RequestFingerprinter, + RequestFingerprinterProtocol, + referer_str, +) -BaseDupeFilterTV = TypeVar("BaseDupeFilterTV", bound="BaseDupeFilter") +if TYPE_CHECKING: + # typing.Self requires Python 3.11 + from typing_extensions import Self + + from scrapy.crawler import Crawler class BaseDupeFilter: @classmethod - def from_settings( - cls: Type[BaseDupeFilterTV], settings: BaseSettings - ) -> BaseDupeFilterTV: + def from_settings(cls, settings: BaseSettings) -> Self: return cls() def request_seen(self, request: Request) -> bool: @@ -36,9 +44,6 @@ class BaseDupeFilter: pass -RFPDupeFilterTV = TypeVar("RFPDupeFilterTV", bound="RFPDupeFilter") - - class RFPDupeFilter(BaseDupeFilter): """Request Fingerprint duplicates filter""" @@ -47,10 +52,12 @@ class RFPDupeFilter(BaseDupeFilter): path: Optional[str] = None, debug: bool = False, *, - fingerprinter=None, + fingerprinter: Optional[RequestFingerprinterProtocol] = None, ) -> None: self.file = None - self.fingerprinter = fingerprinter or RequestFingerprinter() + self.fingerprinter: RequestFingerprinterProtocol = ( + fingerprinter or RequestFingerprinter() + ) self.fingerprints: Set[str] = set() self.logdupes = True self.debug = debug @@ -62,8 +69,11 @@ class RFPDupeFilter(BaseDupeFilter): @classmethod def from_settings( - cls: Type[RFPDupeFilterTV], settings: BaseSettings, *, fingerprinter=None - ) -> RFPDupeFilterTV: + cls, + settings: BaseSettings, + *, + fingerprinter: Optional[RequestFingerprinterProtocol] = None, + ) -> Self: debug = settings.getbool("DUPEFILTER_DEBUG") try: return cls(job_dir(settings), debug, fingerprinter=fingerprinter) @@ -75,11 +85,11 @@ class RFPDupeFilter(BaseDupeFilter): ScrapyDeprecationWarning, ) result = cls(job_dir(settings), debug) - result.fingerprinter = fingerprinter + result.fingerprinter = fingerprinter or RequestFingerprinter() return result @classmethod - def from_crawler(cls, crawler): + def from_crawler(cls, crawler: Crawler) -> Self: try: return cls.from_settings( crawler.settings, diff --git a/scrapy/exceptions.py b/scrapy/exceptions.py index 6e83e4a00..6d188c489 100644 --- a/scrapy/exceptions.py +++ b/scrapy/exceptions.py @@ -52,7 +52,7 @@ class StopDownload(Exception): should be handled by the request errback. Note that 'fail' is a keyword-only argument. """ - def __init__(self, *, fail=True): + def __init__(self, *, fail: bool = True): super().__init__() self.fail = fail diff --git a/scrapy/link.py b/scrapy/link.py index 704649731..0868ae5ef 100644 --- a/scrapy/link.py +++ b/scrapy/link.py @@ -4,6 +4,7 @@ This module defines the Link object used in Link extractors. For actual link extractors implementation see scrapy.linkextractors, or its documentation in: docs/topics/link-extractors.rst """ +from typing import Any class Link: @@ -26,16 +27,20 @@ class Link: __slots__ = ["url", "text", "fragment", "nofollow"] - def __init__(self, url, text="", fragment="", nofollow=False): + def __init__( + self, url: str, text: str = "", fragment: str = "", nofollow: bool = False + ): if not isinstance(url, str): got = url.__class__.__name__ raise TypeError(f"Link urls must be str objects, got {got}") - self.url = url - self.text = text - self.fragment = fragment - self.nofollow = nofollow + self.url: str = url + self.text: str = text + self.fragment: str = fragment + self.nofollow: bool = nofollow - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Link): + raise NotImplementedError return ( self.url == other.url and self.text == other.text @@ -43,12 +48,12 @@ class Link: and self.nofollow == other.nofollow ) - def __hash__(self): + def __hash__(self) -> int: return ( hash(self.url) ^ hash(self.text) ^ hash(self.fragment) ^ hash(self.nofollow) ) - def __repr__(self): + def __repr__(self) -> str: return ( f"Link(url={self.url!r}, text={self.text!r}, " f"fragment={self.fragment!r}, nofollow={self.nofollow!r})" diff --git a/scrapy/logformatter.py b/scrapy/logformatter.py index 7cb379b46..9b05e1153 100644 --- a/scrapy/logformatter.py +++ b/scrapy/logformatter.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import logging import os -from typing import Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Union from twisted.python.failure import Failure @@ -8,6 +10,13 @@ from scrapy import Request, Spider from scrapy.http import Response from scrapy.utils.request import referer_str +if TYPE_CHECKING: + # typing.Self requires Python 3.11 + from typing_extensions import Self + + from scrapy.crawler import Crawler + + SCRAPEDMSG = "Scraped from %(src)s" + os.linesep + "%(item)s" DROPPEDMSG = "Dropped: %(exception)s" + os.linesep + "%(item)s" CRAWLEDMSG = "Crawled (%(status)s) %(request)s%(request_flags)s (referer: %(referer)s)%(response_flags)s" @@ -105,7 +114,7 @@ class LogFormatter: } def item_error( - self, item: Any, exception, response: Response, spider: Spider + self, item: Any, exception: BaseException, response: Response, spider: Spider ) -> dict: """Logs a message when an item causes an error while it is passing through the item pipeline. @@ -161,5 +170,5 @@ class LogFormatter: } @classmethod - def from_crawler(cls, crawler): + def from_crawler(cls, crawler: Crawler) -> Self: return cls() diff --git a/scrapy/middleware.py b/scrapy/middleware.py index 04b838d2d..090588130 100644 --- a/scrapy/middleware.py +++ b/scrapy/middleware.py @@ -1,7 +1,21 @@ +from __future__ import annotations + import logging import pprint from collections import defaultdict, deque -from typing import Any, Callable, Deque, Dict, Iterable, List, Tuple, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Deque, + Dict, + Iterable, + List, + Optional, + Tuple, + Union, + cast, +) from twisted.internet.defer import Deferred @@ -11,6 +25,13 @@ from scrapy.settings import Settings from scrapy.utils.defer import process_chain, process_parallel from scrapy.utils.misc import create_instance, load_object +if TYPE_CHECKING: + # typing.Self requires Python 3.11 + from typing_extensions import Self + + from scrapy.crawler import Crawler + + logger = logging.getLogger(__name__) @@ -34,7 +55,9 @@ class MiddlewareManager: raise NotImplementedError @classmethod - def from_settings(cls, settings: Settings, crawler=None): + def from_settings( + cls, settings: Settings, crawler: Optional[Crawler] = None + ) -> Self: mwlist = cls._get_mwlist_from_settings(settings) middlewares = [] enabled = [] @@ -63,7 +86,7 @@ class MiddlewareManager: return cls(*middlewares) @classmethod - def from_crawler(cls, crawler): + def from_crawler(cls, crawler: Crawler) -> Self: return cls.from_settings(crawler.settings, crawler) def _add_middleware(self, mw: Any) -> None: diff --git a/scrapy/responsetypes.py b/scrapy/responsetypes.py index 58884f21a..9e411d4aa 100644 --- a/scrapy/responsetypes.py +++ b/scrapy/responsetypes.py @@ -5,6 +5,7 @@ based on different criteria. from io import StringIO from mimetypes import MimeTypes from pkgutil import get_data +from typing import Dict, Mapping, Optional, Type, Union from scrapy.http import Response from scrapy.utils.misc import load_object @@ -29,15 +30,19 @@ class ResponseTypes: "text/*": "scrapy.http.TextResponse", } - def __init__(self): - self.classes = {} - self.mimetypes = MimeTypes() - mimedata = get_data("scrapy", "mime.types").decode("utf8") - self.mimetypes.readfp(StringIO(mimedata)) + def __init__(self) -> None: + self.classes: Dict[str, Type[Response]] = {} + self.mimetypes: MimeTypes = MimeTypes() + mimedata = get_data("scrapy", "mime.types") + if not mimedata: + raise ValueError( + "The mime.types file is not found in the Scrapy installation" + ) + self.mimetypes.readfp(StringIO(mimedata.decode("utf8"))) for mimetype, cls in self.CLASSES.items(): self.classes[mimetype] = load_object(cls) - def from_mimetype(self, mimetype): + def from_mimetype(self, mimetype: str) -> Type[Response]: """Return the most appropriate Response class for the given mimetype""" if mimetype is None: return Response @@ -46,7 +51,9 @@ class ResponseTypes: basetype = f"{mimetype.split('/')[0]}/*" return self.classes.get(basetype, Response) - def from_content_type(self, content_type, content_encoding=None): + def from_content_type( + self, content_type: Union[str, bytes], content_encoding: Optional[bytes] = None + ) -> Type[Response]: """Return the most appropriate Response class from an HTTP Content-Type header""" if content_encoding: @@ -56,7 +63,9 @@ class ResponseTypes: ) return self.from_mimetype(mimetype) - def from_content_disposition(self, content_disposition): + def from_content_disposition( + self, content_disposition: Union[str, bytes] + ) -> Type[Response]: try: filename = ( to_unicode(content_disposition, encoding="latin-1", errors="replace") @@ -68,7 +77,7 @@ class ResponseTypes: except IndexError: return Response - def from_headers(self, headers): + def from_headers(self, headers: Mapping[bytes, bytes]) -> Type[Response]: """Return the most appropriate Response class by looking at the HTTP headers""" cls = Response @@ -81,14 +90,14 @@ class ResponseTypes: cls = self.from_content_disposition(headers[b"Content-Disposition"]) return cls - def from_filename(self, filename): + def from_filename(self, filename: str) -> Type[Response]: """Return the most appropriate Response class from a file name""" mimetype, encoding = self.mimetypes.guess_type(filename) if mimetype and not encoding: return self.from_mimetype(mimetype) return Response - def from_body(self, body): + def from_body(self, body: bytes) -> Type[Response]: """Try to guess the appropriate response based on the body content. This method is a bit magic and could be improved in the future, but it's not meant to be used except for special cases where response types @@ -106,7 +115,13 @@ class ResponseTypes: return self.from_mimetype("text/html") return self.from_mimetype("text") - def from_args(self, headers=None, url=None, filename=None, body=None): + def from_args( + self, + headers: Optional[Mapping[bytes, bytes]] = None, + url: Optional[str] = None, + filename: Optional[str] = None, + body: Optional[bytes] = None, + ) -> Type[Response]: """Guess the most appropriate Response class based on the given arguments.""" cls = Response diff --git a/scrapy/spiderloader.py b/scrapy/spiderloader.py index f6bb93ddc..d855c962c 100644 --- a/scrapy/spiderloader.py +++ b/scrapy/spiderloader.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import traceback import warnings from collections import defaultdict from types import ModuleType -from typing import DefaultDict, Dict, List, Tuple, Type +from typing import TYPE_CHECKING, DefaultDict, Dict, List, Tuple, Type from zope.interface import implementer @@ -12,6 +14,10 @@ from scrapy.settings import BaseSettings from scrapy.utils.misc import walk_modules from scrapy.utils.spider import iter_spider_classes +if TYPE_CHECKING: + # typing.Self requires Python 3.11 + from typing_extensions import Self + @implementer(ISpiderLoader) class SpiderLoader: @@ -21,8 +27,8 @@ class SpiderLoader: """ def __init__(self, settings: BaseSettings): - self.spider_modules = settings.getlist("SPIDER_MODULES") - self.warn_only = settings.getbool("SPIDER_LOADER_WARN_ONLY") + self.spider_modules: List[str] = settings.getlist("SPIDER_MODULES") + self.warn_only: bool = settings.getbool("SPIDER_LOADER_WARN_ONLY") self._spiders: Dict[str, Type[Spider]] = {} self._found: DefaultDict[str, List[Tuple[str, str]]] = defaultdict(list) self._load_all_spiders() @@ -69,7 +75,7 @@ class SpiderLoader: self._check_name_duplicates() @classmethod - def from_settings(cls, settings): + def from_settings(cls, settings: BaseSettings) -> Self: return cls(settings) def load(self, spider_name: str) -> Type[Spider]: @@ -90,7 +96,7 @@ class SpiderLoader: name for name, cls in self._spiders.items() if cls.handles_request(request) ] - def list(self): + def list(self) -> List[str]: """ Return a list with the names of all spiders available in the project. """ diff --git a/scrapy/spiders/__init__.py b/scrapy/spiders/__init__.py index 388439f4f..e16d71727 100644 --- a/scrapy/spiders/__init__.py +++ b/scrapy/spiders/__init__.py @@ -6,15 +6,21 @@ See documentation in docs/topics/spiders.rst from __future__ import annotations import logging -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Union, cast + +from twisted.internet.defer import Deferred from scrapy import signals -from scrapy.http import Request +from scrapy.http import Request, Response from scrapy.utils.trackref import object_ref from scrapy.utils.url import url_is_from_spider if TYPE_CHECKING: + # typing.Self requires Python 3.11 + from typing_extensions import Self + from scrapy.crawler import Crawler + from scrapy.settings import BaseSettings class Spider(object_ref): @@ -25,21 +31,21 @@ class Spider(object_ref): name: str custom_settings: Optional[dict] = None - def __init__(self, name=None, **kwargs): + def __init__(self, name: Optional[str] = None, **kwargs: Any): if name is not None: self.name = name elif not getattr(self, "name", None): raise ValueError(f"{type(self).__name__} must have a name") self.__dict__.update(kwargs) if not hasattr(self, "start_urls"): - self.start_urls = [] + self.start_urls: List[str] = [] @property - def logger(self): + def logger(self) -> logging.LoggerAdapter: logger = logging.getLogger(self.name) return logging.LoggerAdapter(logger, {"spider": self}) - def log(self, message, level=logging.DEBUG, **kw): + def log(self, message: Any, level: int = logging.DEBUG, **kw: Any) -> None: """Log the given message at the given log level This helper wraps a log call to the logger within the spider, but you @@ -49,17 +55,17 @@ class Spider(object_ref): self.logger.log(level, message, **kw) @classmethod - def from_crawler(cls, crawler, *args, **kwargs): + def from_crawler(cls, crawler: Crawler, *args: Any, **kwargs: Any) -> Self: spider = cls(*args, **kwargs) spider._set_crawler(crawler) return spider - def _set_crawler(self, crawler: Crawler): + def _set_crawler(self, crawler: Crawler) -> None: self.crawler = crawler self.settings = crawler.settings crawler.signals.connect(self.close, signals.spider_closed) - def start_requests(self): + def start_requests(self) -> Iterable[Request]: if not self.start_urls and hasattr(self, "start_url"): raise AttributeError( "Crawling could not start: 'start_urls' not found " @@ -69,16 +75,16 @@ class Spider(object_ref): for url in self.start_urls: yield Request(url, dont_filter=True) - def _parse(self, response, **kwargs): + def _parse(self, response: Response, **kwargs: Any) -> Any: return self.parse(response, **kwargs) - def parse(self, response, **kwargs): + def parse(self, response: Response, **kwargs: Any) -> Any: raise NotImplementedError( f"{self.__class__.__name__}.parse callback is not defined" ) @classmethod - def update_settings(cls, settings): + def update_settings(cls, settings: BaseSettings) -> None: settings.setdict(cls.custom_settings or {}, priority="spider") @classmethod @@ -86,12 +92,13 @@ class Spider(object_ref): return url_is_from_spider(request.url, cls) @staticmethod - def close(spider, reason): + def close(spider: Spider, reason: str) -> Union[Deferred, None]: closed = getattr(spider, "closed", None) if callable(closed): - return closed(reason) + return cast(Union[Deferred, None], closed(reason)) + return None - def __repr__(self): + def __repr__(self) -> str: return f"<{type(self).__name__} {self.name!r} at 0x{id(self):0x}>" diff --git a/scrapy/utils/request.py b/scrapy/utils/request.py index 6c7f3b345..24fcbd85e 100644 --- a/scrapy/utils/request.py +++ b/scrapy/utils/request.py @@ -14,6 +14,7 @@ from typing import ( Iterable, List, Optional, + Protocol, Tuple, Type, Union, @@ -230,6 +231,11 @@ def fingerprint( return cache[cache_key] +class RequestFingerprinterProtocol(Protocol): + def fingerprint(self, request: Request) -> bytes: + ... + + class RequestFingerprinter: """Default fingerprinter.