mirror of https://github.com/scrapy/scrapy.git
Merge pull request #6333 from wRAR/typing-scrapy
Full typing for scrapy/*.py
This commit is contained in:
commit
563ecbe966
|
|
@ -4,18 +4,22 @@ import inspect
|
|||
import os
|
||||
import sys
|
||||
from importlib.metadata import entry_points
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type
|
||||
|
||||
import scrapy
|
||||
from scrapy.commands import BaseRunSpiderCommand, ScrapyCommand, ScrapyHelpFormatter
|
||||
from scrapy.crawler import CrawlerProcess
|
||||
from scrapy.exceptions import UsageError
|
||||
from scrapy.settings import BaseSettings, Settings
|
||||
from scrapy.utils.misc import walk_modules
|
||||
from scrapy.utils.project import get_project_settings, inside_project
|
||||
from scrapy.utils.python import garbage_collect
|
||||
|
||||
|
||||
class ScrapyArgumentParser(argparse.ArgumentParser):
|
||||
def _parse_optional(self, arg_string):
|
||||
def _parse_optional(
|
||||
self, arg_string: str
|
||||
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
|
||||
# if starts with -: it means that is a parameter not a argument
|
||||
if arg_string[:2] == "-:":
|
||||
return None
|
||||
|
|
@ -23,7 +27,7 @@ class ScrapyArgumentParser(argparse.ArgumentParser):
|
|||
return super()._parse_optional(arg_string)
|
||||
|
||||
|
||||
def _iter_command_classes(module_name):
|
||||
def _iter_command_classes(module_name: str) -> Iterable[Type[ScrapyCommand]]:
|
||||
# TODO: add `name` attribute to commands and merge this function with
|
||||
# scrapy.utils.spider.iter_spider_classes
|
||||
for module in walk_modules(module_name):
|
||||
|
|
@ -37,8 +41,8 @@ def _iter_command_classes(module_name):
|
|||
yield obj
|
||||
|
||||
|
||||
def _get_commands_from_module(module, inproject):
|
||||
d = {}
|
||||
def _get_commands_from_module(module: str, inproject: bool) -> Dict[str, ScrapyCommand]:
|
||||
d: Dict[str, ScrapyCommand] = {}
|
||||
for cmd in _iter_command_classes(module):
|
||||
if inproject or not cmd.requires_project:
|
||||
cmdname = cmd.__module__.split(".")[-1]
|
||||
|
|
@ -46,8 +50,10 @@ def _get_commands_from_module(module, inproject):
|
|||
return d
|
||||
|
||||
|
||||
def _get_commands_from_entry_points(inproject, group="scrapy.commands"):
|
||||
cmds = {}
|
||||
def _get_commands_from_entry_points(
|
||||
inproject: bool, group: str = "scrapy.commands"
|
||||
) -> Dict[str, ScrapyCommand]:
|
||||
cmds: Dict[str, ScrapyCommand] = {}
|
||||
if sys.version_info >= (3, 10):
|
||||
eps = entry_points(group=group)
|
||||
else:
|
||||
|
|
@ -61,7 +67,9 @@ def _get_commands_from_entry_points(inproject, group="scrapy.commands"):
|
|||
return cmds
|
||||
|
||||
|
||||
def _get_commands_dict(settings, inproject):
|
||||
def _get_commands_dict(
|
||||
settings: BaseSettings, inproject: bool
|
||||
) -> Dict[str, ScrapyCommand]:
|
||||
cmds = _get_commands_from_module("scrapy.commands", inproject)
|
||||
cmds.update(_get_commands_from_entry_points(inproject))
|
||||
cmds_module = settings["COMMANDS_MODULE"]
|
||||
|
|
@ -70,16 +78,17 @@ def _get_commands_dict(settings, inproject):
|
|||
return cmds
|
||||
|
||||
|
||||
def _pop_command_name(argv):
|
||||
def _pop_command_name(argv: List[str]) -> Optional[str]:
|
||||
i = 0
|
||||
for arg in argv[1:]:
|
||||
if not arg.startswith("-"):
|
||||
del argv[i]
|
||||
return arg
|
||||
i += 1
|
||||
return None
|
||||
|
||||
|
||||
def _print_header(settings, inproject):
|
||||
def _print_header(settings: BaseSettings, inproject: bool) -> None:
|
||||
version = scrapy.__version__
|
||||
if inproject:
|
||||
print(f"Scrapy {version} - active project: {settings['BOT_NAME']}\n")
|
||||
|
|
@ -88,7 +97,7 @@ def _print_header(settings, inproject):
|
|||
print(f"Scrapy {version} - no active project\n")
|
||||
|
||||
|
||||
def _print_commands(settings, inproject):
|
||||
def _print_commands(settings: BaseSettings, inproject: bool) -> None:
|
||||
_print_header(settings, inproject)
|
||||
print("Usage:")
|
||||
print(" scrapy <command> [options] [args]\n")
|
||||
|
|
@ -103,13 +112,17 @@ def _print_commands(settings, inproject):
|
|||
print('Use "scrapy <command> -h" to see more info about a command')
|
||||
|
||||
|
||||
def _print_unknown_command(settings, cmdname, inproject):
|
||||
def _print_unknown_command(
|
||||
settings: BaseSettings, cmdname: str, inproject: bool
|
||||
) -> None:
|
||||
_print_header(settings, inproject)
|
||||
print(f"Unknown command: {cmdname}\n")
|
||||
print('Use "scrapy" to see available commands')
|
||||
|
||||
|
||||
def _run_print_help(parser, func, *a, **kw):
|
||||
def _run_print_help(
|
||||
parser: argparse.ArgumentParser, func: Callable, *a: Any, **kw: Any
|
||||
) -> None:
|
||||
try:
|
||||
func(*a, **kw)
|
||||
except UsageError as e:
|
||||
|
|
@ -120,7 +133,9 @@ def _run_print_help(parser, func, *a, **kw):
|
|||
sys.exit(2)
|
||||
|
||||
|
||||
def execute(argv=None, settings=None):
|
||||
def execute(
|
||||
argv: Optional[List[str]] = None, settings: Optional[Settings] = None
|
||||
) -> None:
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
|
|
@ -162,14 +177,16 @@ def execute(argv=None, settings=None):
|
|||
sys.exit(cmd.exitcode)
|
||||
|
||||
|
||||
def _run_command(cmd, args, opts):
|
||||
def _run_command(cmd: ScrapyCommand, args: List[str], opts: argparse.Namespace) -> None:
|
||||
if opts.profile:
|
||||
_run_command_profiled(cmd, args, opts)
|
||||
else:
|
||||
cmd.run(args, opts)
|
||||
|
||||
|
||||
def _run_command_profiled(cmd, args, opts):
|
||||
def _run_command_profiled(
|
||||
cmd: ScrapyCommand, args: List[str], opts: argparse.Namespace
|
||||
) -> None:
|
||||
if opts.profile:
|
||||
sys.stderr.write(f"scrapy: writing cProfile stats to {opts.profile!r}\n")
|
||||
loc = locals()
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ class Downloader:
|
|||
|
||||
return key, self.slots[key]
|
||||
|
||||
def _get_slot_key(self, request: Request, spider: Spider) -> str:
|
||||
def _get_slot_key(self, request: Request, spider: Optional[Spider]) -> str:
|
||||
if self.DOWNLOAD_SLOT in request.meta:
|
||||
return cast(str, request.meta[self.DOWNLOAD_SLOT])
|
||||
|
||||
|
|
|
|||
|
|
@ -4,19 +4,23 @@ import json
|
|||
import logging
|
||||
from abc import abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Optional, Type, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, Any, Optional, Type, cast
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
|
||||
from scrapy.crawler import Crawler
|
||||
from scrapy.dupefilters import BaseDupeFilter
|
||||
from scrapy.http.request import Request
|
||||
from scrapy.pqueues import ScrapyPriorityQueue
|
||||
from scrapy.spiders import Spider
|
||||
from scrapy.statscollectors import StatsCollector
|
||||
from scrapy.utils.job import job_dir
|
||||
from scrapy.utils.misc import build_from_crawler, load_object
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# requires queuelib >= 1.6.2
|
||||
from queuelib.queue import BaseQueue
|
||||
|
||||
# typing.Self requires Python 3.11
|
||||
from typing_extensions import Self
|
||||
|
||||
|
|
@ -121,9 +125,6 @@ class BaseScheduler(metaclass=BaseSchedulerMeta):
|
|||
raise NotImplementedError()
|
||||
|
||||
|
||||
SchedulerTV = TypeVar("SchedulerTV", bound="Scheduler")
|
||||
|
||||
|
||||
class Scheduler(BaseScheduler):
|
||||
"""
|
||||
Default Scrapy scheduler. This implementation also handles duplication
|
||||
|
|
@ -179,24 +180,24 @@ class Scheduler(BaseScheduler):
|
|||
self,
|
||||
dupefilter: BaseDupeFilter,
|
||||
jobdir: Optional[str] = None,
|
||||
dqclass=None,
|
||||
mqclass=None,
|
||||
dqclass: Optional[Type[BaseQueue]] = None,
|
||||
mqclass: Optional[Type[BaseQueue]] = None,
|
||||
logunser: bool = False,
|
||||
stats: Optional[StatsCollector] = None,
|
||||
pqclass=None,
|
||||
pqclass: Optional[Type[ScrapyPriorityQueue]] = None,
|
||||
crawler: Optional[Crawler] = None,
|
||||
):
|
||||
self.df: BaseDupeFilter = dupefilter
|
||||
self.dqdir: Optional[str] = self._dqdir(jobdir)
|
||||
self.pqclass = pqclass
|
||||
self.dqclass = dqclass
|
||||
self.mqclass = mqclass
|
||||
self.pqclass: Optional[Type[ScrapyPriorityQueue]] = pqclass
|
||||
self.dqclass: Optional[Type[BaseQueue]] = dqclass
|
||||
self.mqclass: Optional[Type[BaseQueue]] = mqclass
|
||||
self.logunser: bool = logunser
|
||||
self.stats: Optional[StatsCollector] = stats
|
||||
self.crawler: Optional[Crawler] = crawler
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls: Type[SchedulerTV], crawler: Crawler) -> SchedulerTV:
|
||||
def from_crawler(cls, crawler: Crawler) -> Self:
|
||||
"""
|
||||
Factory method, initializes the scheduler with arguments taken from the crawl settings
|
||||
"""
|
||||
|
|
@ -221,9 +222,9 @@ class Scheduler(BaseScheduler):
|
|||
(2) initialize the disk queue if the ``jobdir`` attribute is a valid directory
|
||||
(3) return the result of the dupefilter's ``open`` method
|
||||
"""
|
||||
self.spider = spider
|
||||
self.mqs = self._mq()
|
||||
self.dqs = self._dq() if self.dqdir else None
|
||||
self.spider: Spider = spider
|
||||
self.mqs: ScrapyPriorityQueue = self._mq()
|
||||
self.dqs: Optional[ScrapyPriorityQueue] = self._dq() if self.dqdir else None
|
||||
return self.df.open()
|
||||
|
||||
def close(self, reason: str) -> Optional[Deferred]:
|
||||
|
|
@ -320,9 +321,10 @@ class Scheduler(BaseScheduler):
|
|||
return self.dqs.pop()
|
||||
return None
|
||||
|
||||
def _mq(self):
|
||||
def _mq(self) -> ScrapyPriorityQueue:
|
||||
"""Create a new priority queue instance, with in-memory storage"""
|
||||
assert self.crawler
|
||||
assert self.pqclass
|
||||
return build_from_crawler(
|
||||
self.pqclass,
|
||||
self.crawler,
|
||||
|
|
@ -330,10 +332,11 @@ class Scheduler(BaseScheduler):
|
|||
key="",
|
||||
)
|
||||
|
||||
def _dq(self):
|
||||
def _dq(self) -> ScrapyPriorityQueue:
|
||||
"""Create a new priority queue instance, with disk storage"""
|
||||
assert self.crawler
|
||||
assert self.dqdir
|
||||
assert self.pqclass
|
||||
state = self._read_dqs_state(self.dqdir)
|
||||
q = build_from_crawler(
|
||||
self.pqclass,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ The Extension Manager
|
|||
See documentation in docs/topics/extensions.rst
|
||||
"""
|
||||
|
||||
from typing import Any, List
|
||||
|
||||
from scrapy.middleware import MiddlewareManager
|
||||
from scrapy.settings import Settings
|
||||
from scrapy.utils.conf import build_component_list
|
||||
|
||||
|
||||
|
|
@ -12,5 +15,5 @@ class ExtensionManager(MiddlewareManager):
|
|||
component_name = "extension"
|
||||
|
||||
@classmethod
|
||||
def _get_mwlist_from_settings(cls, settings):
|
||||
def _get_mwlist_from_settings(cls, settings: Settings) -> List[Any]:
|
||||
return build_component_list(settings.getwithbase("EXTENSIONS"))
|
||||
|
|
|
|||
|
|
@ -4,14 +4,28 @@ Scrapy Item
|
|||
See documentation in docs/topics/item.rst
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABCMeta
|
||||
from collections.abc import MutableMapping
|
||||
from copy import deepcopy
|
||||
from pprint import pformat
|
||||
from typing import Dict
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterator,
|
||||
KeysView,
|
||||
MutableMapping,
|
||||
NoReturn,
|
||||
Tuple,
|
||||
)
|
||||
|
||||
from scrapy.utils.trackref import object_ref
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# typing.Self requires Python 3.11
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
class Field(dict):
|
||||
"""Container of field metadata"""
|
||||
|
|
@ -23,7 +37,9 @@ class ItemMeta(ABCMeta):
|
|||
.. _metaclass: https://realpython.com/python-metaclasses
|
||||
"""
|
||||
|
||||
def __new__(mcs, class_name, bases, attrs):
|
||||
def __new__(
|
||||
mcs, class_name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]
|
||||
) -> ItemMeta:
|
||||
classcell = attrs.pop("__classcell__", None)
|
||||
new_bases = tuple(base._class for base in bases if hasattr(base, "_class"))
|
||||
_class = super().__new__(mcs, "x_" + class_name, new_bases, attrs)
|
||||
|
|
@ -44,7 +60,7 @@ class ItemMeta(ABCMeta):
|
|||
return super().__new__(mcs, class_name, bases, new_attrs)
|
||||
|
||||
|
||||
class Item(MutableMapping, object_ref, metaclass=ItemMeta):
|
||||
class Item(MutableMapping[str, Any], object_ref, metaclass=ItemMeta):
|
||||
"""
|
||||
Base class for scraped items.
|
||||
|
||||
|
|
@ -69,51 +85,51 @@ class Item(MutableMapping, object_ref, metaclass=ItemMeta):
|
|||
|
||||
fields: Dict[str, Field]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._values = {}
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
self._values: Dict[str, Any] = {}
|
||||
if args or kwargs: # avoid creating dict for most common case
|
||||
for k, v in dict(*args, **kwargs).items():
|
||||
self[k] = v
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
return self._values[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
if key in self.fields:
|
||||
self._values[key] = value
|
||||
else:
|
||||
raise KeyError(f"{self.__class__.__name__} does not support field: {key}")
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
del self._values[key]
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, name: str) -> NoReturn:
|
||||
if name in self.fields:
|
||||
raise AttributeError(f"Use item[{name!r}] to get field value")
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if not name.startswith("_"):
|
||||
raise AttributeError(f"Use item[{name!r}] = {value!r} to set field value")
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return len(self._values)
|
||||
|
||||
def __iter__(self):
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self._values)
|
||||
|
||||
__hash__ = object_ref.__hash__
|
||||
|
||||
def keys(self):
|
||||
def keys(self) -> KeysView[str]:
|
||||
return self._values.keys()
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return pformat(dict(self))
|
||||
|
||||
def copy(self):
|
||||
def copy(self) -> Self:
|
||||
return self.__class__(self)
|
||||
|
||||
def deepcopy(self):
|
||||
def deepcopy(self) -> Self:
|
||||
"""Return a :func:`~copy.deepcopy` of this item."""
|
||||
return deepcopy(self)
|
||||
|
|
|
|||
|
|
@ -14,11 +14,23 @@ from email.mime.nonmultipart import MIMENonMultipart
|
|||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate
|
||||
from io import BytesIO
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from twisted import version as twisted_version
|
||||
from twisted.internet import ssl
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.versions import Version
|
||||
|
||||
from scrapy.settings import BaseSettings
|
||||
|
|
@ -26,6 +38,9 @@ from scrapy.utils.misc import arg_to_iter
|
|||
from scrapy.utils.python import to_bytes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# imports twisted.internet.reactor
|
||||
from twisted.mail.smtp import ESMTPSenderFactory
|
||||
|
||||
# typing.Self requires Python 3.11
|
||||
from typing_extensions import Self
|
||||
|
||||
|
|
@ -37,7 +52,7 @@ logger = logging.getLogger(__name__)
|
|||
COMMASPACE = ", "
|
||||
|
||||
|
||||
def _to_bytes_or_none(text):
|
||||
def _to_bytes_or_none(text: Union[str, bytes, None]) -> Optional[bytes]:
|
||||
if text is None:
|
||||
return None
|
||||
return to_bytes(text)
|
||||
|
|
@ -46,23 +61,23 @@ def _to_bytes_or_none(text):
|
|||
class MailSender:
|
||||
def __init__(
|
||||
self,
|
||||
smtphost="localhost",
|
||||
mailfrom="scrapy@localhost",
|
||||
smtpuser=None,
|
||||
smtppass=None,
|
||||
smtpport=25,
|
||||
smtptls=False,
|
||||
smtpssl=False,
|
||||
debug=False,
|
||||
smtphost: str = "localhost",
|
||||
mailfrom: str = "scrapy@localhost",
|
||||
smtpuser: Optional[str] = None,
|
||||
smtppass: Optional[str] = None,
|
||||
smtpport: int = 25,
|
||||
smtptls: bool = False,
|
||||
smtpssl: bool = False,
|
||||
debug: bool = False,
|
||||
):
|
||||
self.smtphost = smtphost
|
||||
self.smtpport = smtpport
|
||||
self.smtpuser = _to_bytes_or_none(smtpuser)
|
||||
self.smtppass = _to_bytes_or_none(smtppass)
|
||||
self.smtptls = smtptls
|
||||
self.smtpssl = smtpssl
|
||||
self.mailfrom = mailfrom
|
||||
self.debug = debug
|
||||
self.smtphost: str = smtphost
|
||||
self.smtpport: int = smtpport
|
||||
self.smtpuser: Optional[bytes] = _to_bytes_or_none(smtpuser)
|
||||
self.smtppass: Optional[bytes] = _to_bytes_or_none(smtppass)
|
||||
self.smtptls: bool = smtptls
|
||||
self.smtpssl: bool = smtpssl
|
||||
self.mailfrom: str = mailfrom
|
||||
self.debug: bool = debug
|
||||
|
||||
@classmethod
|
||||
def from_settings(cls, settings: BaseSettings) -> Self:
|
||||
|
|
@ -78,14 +93,14 @@ class MailSender:
|
|||
|
||||
def send(
|
||||
self,
|
||||
to,
|
||||
subject,
|
||||
body,
|
||||
cc=None,
|
||||
attachs=(),
|
||||
mimetype="text/plain",
|
||||
charset=None,
|
||||
_callback=None,
|
||||
to: Union[str, List[str]],
|
||||
subject: str,
|
||||
body: str,
|
||||
cc: Union[str, List[str], None] = None,
|
||||
attachs: Sequence[Tuple[str, str, IO]] = (),
|
||||
mimetype: str = "text/plain",
|
||||
charset: Optional[str] = None,
|
||||
_callback: Optional[Callable[..., None]] = None,
|
||||
) -> Optional[Deferred]:
|
||||
from twisted.internet import reactor
|
||||
|
||||
|
|
@ -142,13 +157,15 @@ class MailSender:
|
|||
dfd.addCallbacks(
|
||||
callback=self._sent_ok,
|
||||
errback=self._sent_failed,
|
||||
callbackArgs=[to, cc, subject, len(attachs)],
|
||||
errbackArgs=[to, cc, subject, len(attachs)],
|
||||
callbackArgs=(to, cc, subject, len(attachs)),
|
||||
errbackArgs=(to, cc, subject, len(attachs)),
|
||||
)
|
||||
reactor.addSystemEventTrigger("before", "shutdown", lambda: dfd)
|
||||
return dfd
|
||||
|
||||
def _sent_ok(self, result, to, cc, subject, nattachs):
|
||||
def _sent_ok(
|
||||
self, result: Any, to: List[str], cc: List[str], subject: str, nattachs: int
|
||||
) -> None:
|
||||
logger.info(
|
||||
"Mail sent OK: To=%(mailto)s Cc=%(mailcc)s "
|
||||
'Subject="%(mailsubject)s" Attachs=%(mailattachs)d',
|
||||
|
|
@ -160,7 +177,14 @@ class MailSender:
|
|||
},
|
||||
)
|
||||
|
||||
def _sent_failed(self, failure, to, cc, subject, nattachs):
|
||||
def _sent_failed(
|
||||
self,
|
||||
failure: Failure,
|
||||
to: List[str],
|
||||
cc: List[str],
|
||||
subject: str,
|
||||
nattachs: int,
|
||||
) -> Failure:
|
||||
errstr = str(failure.value)
|
||||
logger.error(
|
||||
"Unable to send mail: To=%(mailto)s Cc=%(mailcc)s "
|
||||
|
|
@ -176,13 +200,13 @@ class MailSender:
|
|||
)
|
||||
return failure
|
||||
|
||||
def _sendmail(self, to_addrs, msg):
|
||||
def _sendmail(self, to_addrs: List[str], msg: bytes) -> Deferred:
|
||||
from twisted.internet import reactor
|
||||
|
||||
msg = BytesIO(msg)
|
||||
d = Deferred()
|
||||
msg_io = BytesIO(msg)
|
||||
d: Deferred = Deferred()
|
||||
|
||||
factory = self._create_sender_factory(to_addrs, msg, d)
|
||||
factory = self._create_sender_factory(to_addrs, msg_io, d)
|
||||
|
||||
if self.smtpssl:
|
||||
reactor.connectSSL(
|
||||
|
|
@ -193,10 +217,12 @@ class MailSender:
|
|||
|
||||
return d
|
||||
|
||||
def _create_sender_factory(self, to_addrs, msg, d):
|
||||
def _create_sender_factory(
|
||||
self, to_addrs: List[str], msg: IO, d: Deferred
|
||||
) -> ESMTPSenderFactory:
|
||||
from twisted.mail.smtp import ESMTPSenderFactory
|
||||
|
||||
factory_keywords = {
|
||||
factory_keywords: Dict[str, Any] = {
|
||||
"heloFallback": True,
|
||||
"requireAuthentication": False,
|
||||
"requireTransportSecurity": self.smtptls,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,33 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Protocol,
|
||||
Tuple,
|
||||
Type,
|
||||
cast,
|
||||
)
|
||||
|
||||
from scrapy import Request
|
||||
from scrapy.core.downloader import Downloader
|
||||
from scrapy.utils.misc import build_from_crawler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# typing.Self requires Python 3.11
|
||||
from typing_extensions import Self
|
||||
|
||||
from scrapy.crawler import Crawler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _path_safe(text):
|
||||
def _path_safe(text: str) -> str:
|
||||
"""
|
||||
Return a filesystem-safe version of a string ``text``
|
||||
|
||||
|
|
@ -24,6 +45,18 @@ def _path_safe(text):
|
|||
return "-".join([pathable_slot, unique_slot])
|
||||
|
||||
|
||||
class QueueProtocol(Protocol):
|
||||
"""Protocol for downstream queues of ``ScrapyPriorityQueue``."""
|
||||
|
||||
def push(self, request: Request) -> None: ...
|
||||
|
||||
def pop(self) -> Optional[Request]: ...
|
||||
|
||||
def close(self) -> None: ...
|
||||
|
||||
def __len__(self) -> int: ...
|
||||
|
||||
|
||||
class ScrapyPriorityQueue:
|
||||
"""A priority queue implemented using multiple internal queues (typically,
|
||||
FIFO queues). It uses one internal queue for each priority value. The internal
|
||||
|
|
@ -51,18 +84,30 @@ class ScrapyPriorityQueue:
|
|||
"""
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, downstream_queue_cls, key, startprios=()):
|
||||
def from_crawler(
|
||||
cls,
|
||||
crawler: Crawler,
|
||||
downstream_queue_cls: Type[QueueProtocol],
|
||||
key: str,
|
||||
startprios: Iterable[int] = (),
|
||||
) -> Self:
|
||||
return cls(crawler, downstream_queue_cls, key, startprios)
|
||||
|
||||
def __init__(self, crawler, downstream_queue_cls, key, startprios=()):
|
||||
self.crawler = crawler
|
||||
self.downstream_queue_cls = downstream_queue_cls
|
||||
self.key = key
|
||||
self.queues = {}
|
||||
self.curprio = None
|
||||
def __init__(
|
||||
self,
|
||||
crawler: Crawler,
|
||||
downstream_queue_cls: Type[QueueProtocol],
|
||||
key: str,
|
||||
startprios: Iterable[int] = (),
|
||||
):
|
||||
self.crawler: Crawler = crawler
|
||||
self.downstream_queue_cls: Type[QueueProtocol] = downstream_queue_cls
|
||||
self.key: str = key
|
||||
self.queues: Dict[int, QueueProtocol] = {}
|
||||
self.curprio: Optional[int] = None
|
||||
self.init_prios(startprios)
|
||||
|
||||
def init_prios(self, startprios):
|
||||
def init_prios(self, startprios: Iterable[int]) -> None:
|
||||
if not startprios:
|
||||
return
|
||||
|
||||
|
|
@ -71,17 +116,17 @@ class ScrapyPriorityQueue:
|
|||
|
||||
self.curprio = min(startprios)
|
||||
|
||||
def qfactory(self, key):
|
||||
def qfactory(self, key: int) -> QueueProtocol:
|
||||
return build_from_crawler(
|
||||
self.downstream_queue_cls,
|
||||
self.crawler,
|
||||
self.key + "/" + str(key),
|
||||
)
|
||||
|
||||
def priority(self, request):
|
||||
def priority(self, request: Request) -> int:
|
||||
return -request.priority
|
||||
|
||||
def push(self, request):
|
||||
def push(self, request: Request) -> None:
|
||||
priority = self.priority(request)
|
||||
if priority not in self.queues:
|
||||
self.queues[priority] = self.qfactory(priority)
|
||||
|
|
@ -90,9 +135,9 @@ class ScrapyPriorityQueue:
|
|||
if self.curprio is None or priority < self.curprio:
|
||||
self.curprio = priority
|
||||
|
||||
def pop(self):
|
||||
def pop(self) -> Optional[Request]:
|
||||
if self.curprio is None:
|
||||
return
|
||||
return None
|
||||
q = self.queues[self.curprio]
|
||||
m = q.pop()
|
||||
if not q:
|
||||
|
|
@ -102,7 +147,7 @@ class ScrapyPriorityQueue:
|
|||
self.curprio = min(prios) if prios else None
|
||||
return m
|
||||
|
||||
def peek(self):
|
||||
def peek(self) -> Optional[Request]:
|
||||
"""Returns the next object to be returned by :meth:`pop`,
|
||||
but without removing it from the queue.
|
||||
|
||||
|
|
@ -112,30 +157,32 @@ class ScrapyPriorityQueue:
|
|||
if self.curprio is None:
|
||||
return None
|
||||
queue = self.queues[self.curprio]
|
||||
return queue.peek()
|
||||
# Protocols can't declare optional members
|
||||
return cast(Request, queue.peek()) # type: ignore[attr-defined]
|
||||
|
||||
def close(self):
|
||||
active = []
|
||||
def close(self) -> List[int]:
|
||||
active: List[int] = []
|
||||
for p, q in self.queues.items():
|
||||
active.append(p)
|
||||
q.close()
|
||||
return active
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return sum(len(x) for x in self.queues.values()) if self.queues else 0
|
||||
|
||||
|
||||
class DownloaderInterface:
|
||||
def __init__(self, crawler):
|
||||
self.downloader = crawler.engine.downloader
|
||||
def __init__(self, crawler: Crawler):
|
||||
assert crawler.engine
|
||||
self.downloader: Downloader = crawler.engine.downloader
|
||||
|
||||
def stats(self, possible_slots):
|
||||
def stats(self, possible_slots: Iterable[str]) -> List[Tuple[int, str]]:
|
||||
return [(self._active_downloads(slot), slot) for slot in possible_slots]
|
||||
|
||||
def get_slot_key(self, request):
|
||||
def get_slot_key(self, request: Request) -> str:
|
||||
return self.downloader._get_slot_key(request, None)
|
||||
|
||||
def _active_downloads(self, slot):
|
||||
def _active_downloads(self, slot: str) -> int:
|
||||
"""Return a number of requests in a Downloader for a given slot"""
|
||||
if slot not in self.downloader.slots:
|
||||
return 0
|
||||
|
|
@ -149,10 +196,22 @@ class DownloaderAwarePriorityQueue:
|
|||
"""
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, downstream_queue_cls, key, startprios=()):
|
||||
def from_crawler(
|
||||
cls,
|
||||
crawler: Crawler,
|
||||
downstream_queue_cls: Type[QueueProtocol],
|
||||
key: str,
|
||||
startprios: Optional[Dict[str, Iterable[int]]] = None,
|
||||
) -> Self:
|
||||
return cls(crawler, downstream_queue_cls, key, startprios)
|
||||
|
||||
def __init__(self, crawler, downstream_queue_cls, key, slot_startprios=()):
|
||||
def __init__(
|
||||
self,
|
||||
crawler: Crawler,
|
||||
downstream_queue_cls: Type[QueueProtocol],
|
||||
key: str,
|
||||
slot_startprios: Optional[Dict[str, Iterable[int]]] = None,
|
||||
):
|
||||
if crawler.settings.getint("CONCURRENT_REQUESTS_PER_IP") != 0:
|
||||
raise ValueError(
|
||||
f'"{self.__class__}" does not support CONCURRENT_REQUESTS_PER_IP'
|
||||
|
|
@ -169,16 +228,18 @@ class DownloaderAwarePriorityQueue:
|
|||
"queue class can be resumed."
|
||||
)
|
||||
|
||||
self._downloader_interface = DownloaderInterface(crawler)
|
||||
self.downstream_queue_cls = downstream_queue_cls
|
||||
self.key = key
|
||||
self.crawler = crawler
|
||||
self._downloader_interface: DownloaderInterface = DownloaderInterface(crawler)
|
||||
self.downstream_queue_cls: Type[QueueProtocol] = downstream_queue_cls
|
||||
self.key: str = key
|
||||
self.crawler: Crawler = crawler
|
||||
|
||||
self.pqueues = {} # slot -> priority queue
|
||||
self.pqueues: Dict[str, ScrapyPriorityQueue] = {} # slot -> priority queue
|
||||
for slot, startprios in (slot_startprios or {}).items():
|
||||
self.pqueues[slot] = self.pqfactory(slot, startprios)
|
||||
|
||||
def pqfactory(self, slot, startprios=()):
|
||||
def pqfactory(
|
||||
self, slot: str, startprios: Iterable[int] = ()
|
||||
) -> ScrapyPriorityQueue:
|
||||
return ScrapyPriorityQueue(
|
||||
self.crawler,
|
||||
self.downstream_queue_cls,
|
||||
|
|
@ -186,11 +247,11 @@ class DownloaderAwarePriorityQueue:
|
|||
startprios,
|
||||
)
|
||||
|
||||
def pop(self):
|
||||
def pop(self) -> Optional[Request]:
|
||||
stats = self._downloader_interface.stats(self.pqueues)
|
||||
|
||||
if not stats:
|
||||
return
|
||||
return None
|
||||
|
||||
slot = min(stats)[1]
|
||||
queue = self.pqueues[slot]
|
||||
|
|
@ -199,14 +260,14 @@ class DownloaderAwarePriorityQueue:
|
|||
del self.pqueues[slot]
|
||||
return request
|
||||
|
||||
def push(self, request):
|
||||
def push(self, request: Request) -> None:
|
||||
slot = self._downloader_interface.get_slot_key(request)
|
||||
if slot not in self.pqueues:
|
||||
self.pqueues[slot] = self.pqfactory(slot)
|
||||
queue = self.pqueues[slot]
|
||||
queue.push(request)
|
||||
|
||||
def peek(self):
|
||||
def peek(self) -> Optional[Request]:
|
||||
"""Returns the next object to be returned by :meth:`pop`,
|
||||
but without removing it from the queue.
|
||||
|
||||
|
|
@ -220,13 +281,13 @@ class DownloaderAwarePriorityQueue:
|
|||
queue = self.pqueues[slot]
|
||||
return queue.peek()
|
||||
|
||||
def close(self):
|
||||
def close(self) -> Dict[str, List[int]]:
|
||||
active = {slot: queue.close() for slot, queue in self.pqueues.items()}
|
||||
self.pqueues.clear()
|
||||
return active
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
return sum(len(x) for x in self.pqueues.values()) if self.pqueues else 0
|
||||
|
||||
def __contains__(self, slot):
|
||||
def __contains__(self, slot: str) -> bool:
|
||||
return slot in self.pqueues
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
from typing import Any
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Type
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet.base import ThreadedResolver
|
||||
from twisted.internet.base import ReactorBase, ThreadedResolver
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.interfaces import (
|
||||
IAddress,
|
||||
IHostnameResolver,
|
||||
IHostResolution,
|
||||
IResolutionReceiver,
|
||||
|
|
@ -12,6 +16,12 @@ from zope.interface.declarations import implementer, provider
|
|||
|
||||
from scrapy.utils.datatypes import LocalCache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# typing.Self requires Python 3.11
|
||||
from typing_extensions import Self
|
||||
|
||||
from scrapy.crawler import Crawler
|
||||
|
||||
# TODO: cache misses
|
||||
dnscache: LocalCache[str, Any] = LocalCache(10000)
|
||||
|
||||
|
|
@ -22,65 +32,66 @@ class CachingThreadedResolver(ThreadedResolver):
|
|||
Default caching resolver. IPv4 only, supports setting a timeout value for DNS requests.
|
||||
"""
|
||||
|
||||
def __init__(self, reactor, cache_size, timeout):
|
||||
def __init__(self, reactor: ReactorBase, cache_size: int, timeout: float):
|
||||
super().__init__(reactor)
|
||||
dnscache.limit = cache_size
|
||||
self.timeout = timeout
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, reactor):
|
||||
def from_crawler(cls, crawler: Crawler, reactor: ReactorBase) -> Self:
|
||||
if crawler.settings.getbool("DNSCACHE_ENABLED"):
|
||||
cache_size = crawler.settings.getint("DNSCACHE_SIZE")
|
||||
else:
|
||||
cache_size = 0
|
||||
return cls(reactor, cache_size, crawler.settings.getfloat("DNS_TIMEOUT"))
|
||||
|
||||
def install_on_reactor(self):
|
||||
def install_on_reactor(self) -> None:
|
||||
self.reactor.installResolver(self)
|
||||
|
||||
def getHostByName(self, name: str, timeout=None):
|
||||
def getHostByName(self, name: str, timeout: Sequence[int] = ()) -> Deferred[str]:
|
||||
if name in dnscache:
|
||||
return defer.succeed(dnscache[name])
|
||||
# in Twisted<=16.6, getHostByName() is always called with
|
||||
# a default timeout of 60s (actually passed as (1, 3, 11, 45) tuple),
|
||||
# so the input argument above is simply overridden
|
||||
# to enforce Scrapy's DNS_TIMEOUT setting's value
|
||||
timeout = (self.timeout,)
|
||||
# The timeout arg is typed as Sequence[int] but supports floats.
|
||||
timeout = (self.timeout,) # type: ignore[assignment]
|
||||
d = super().getHostByName(name, timeout)
|
||||
if dnscache.limit:
|
||||
d.addCallback(self._cache_result, name)
|
||||
return d
|
||||
|
||||
def _cache_result(self, result, name):
|
||||
def _cache_result(self, result: Any, name: str) -> Any:
|
||||
dnscache[name] = result
|
||||
return result
|
||||
|
||||
|
||||
@implementer(IHostResolution)
|
||||
class HostResolution:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
def __init__(self, name: str):
|
||||
self.name: str = name
|
||||
|
||||
def cancel(self):
|
||||
def cancel(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@provider(IResolutionReceiver)
|
||||
class _CachingResolutionReceiver:
|
||||
def __init__(self, resolutionReceiver, hostName):
|
||||
self.resolutionReceiver = resolutionReceiver
|
||||
self.hostName = hostName
|
||||
self.addresses = []
|
||||
def __init__(self, resolutionReceiver: IResolutionReceiver, hostName: str):
|
||||
self.resolutionReceiver: IResolutionReceiver = resolutionReceiver
|
||||
self.hostName: str = hostName
|
||||
self.addresses: List[IAddress] = []
|
||||
|
||||
def resolutionBegan(self, resolution):
|
||||
def resolutionBegan(self, resolution: IHostResolution) -> None:
|
||||
self.resolutionReceiver.resolutionBegan(resolution)
|
||||
self.resolution = resolution
|
||||
|
||||
def addressResolved(self, address):
|
||||
def addressResolved(self, address: IAddress) -> None:
|
||||
self.resolutionReceiver.addressResolved(address)
|
||||
self.addresses.append(address)
|
||||
|
||||
def resolutionComplete(self):
|
||||
def resolutionComplete(self) -> None:
|
||||
self.resolutionReceiver.resolutionComplete()
|
||||
if self.addresses:
|
||||
dnscache[self.hostName] = self.addresses
|
||||
|
|
@ -93,30 +104,30 @@ class CachingHostnameResolver:
|
|||
does not support setting a timeout value for DNS requests.
|
||||
"""
|
||||
|
||||
def __init__(self, reactor, cache_size):
|
||||
self.reactor = reactor
|
||||
self.original_resolver = reactor.nameResolver
|
||||
def __init__(self, reactor: ReactorBase, cache_size: int):
|
||||
self.reactor: ReactorBase = reactor
|
||||
self.original_resolver: IHostnameResolver = reactor.nameResolver
|
||||
dnscache.limit = cache_size
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, reactor):
|
||||
def from_crawler(cls, crawler: Crawler, reactor: ReactorBase) -> Self:
|
||||
if crawler.settings.getbool("DNSCACHE_ENABLED"):
|
||||
cache_size = crawler.settings.getint("DNSCACHE_SIZE")
|
||||
else:
|
||||
cache_size = 0
|
||||
return cls(reactor, cache_size)
|
||||
|
||||
def install_on_reactor(self):
|
||||
def install_on_reactor(self) -> None:
|
||||
self.reactor.installNameResolver(self)
|
||||
|
||||
def resolveHostName(
|
||||
self,
|
||||
resolutionReceiver,
|
||||
resolutionReceiver: IResolutionReceiver,
|
||||
hostName: str,
|
||||
portNumber=0,
|
||||
addressTypes=None,
|
||||
transportSemantics="TCP",
|
||||
):
|
||||
portNumber: int = 0,
|
||||
addressTypes: Optional[Sequence[Type[IAddress]]] = None,
|
||||
transportSemantics: str = "TCP",
|
||||
) -> IHostResolution:
|
||||
try:
|
||||
addresses = dnscache[hostName]
|
||||
except KeyError:
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ from __future__ import annotations
|
|||
import logging
|
||||
import sys
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import TYPE_CHECKING, Union
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
from warnings import warn
|
||||
|
||||
from scrapy import Spider
|
||||
from scrapy.exceptions import ScrapyDeprecationWarning
|
||||
from scrapy.utils.python import to_unicode
|
||||
|
||||
|
|
@ -18,12 +19,14 @@ if TYPE_CHECKING:
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def decode_robotstxt(robotstxt_body, spider, to_native_str_type=False):
|
||||
def decode_robotstxt(
|
||||
robotstxt_body: bytes, spider: Optional[Spider], to_native_str_type: bool = False
|
||||
) -> str:
|
||||
try:
|
||||
if to_native_str_type:
|
||||
robotstxt_body = to_unicode(robotstxt_body)
|
||||
body_decoded = to_unicode(robotstxt_body)
|
||||
else:
|
||||
robotstxt_body = robotstxt_body.decode("utf-8", errors="ignore")
|
||||
body_decoded = robotstxt_body.decode("utf-8", errors="ignore")
|
||||
except UnicodeDecodeError:
|
||||
# If we found garbage or robots.txt in an encoding other than UTF-8, disregard it.
|
||||
# Switch to 'allow all' state.
|
||||
|
|
@ -33,8 +36,8 @@ def decode_robotstxt(robotstxt_body, spider, to_native_str_type=False):
|
|||
exc_info=sys.exc_info(),
|
||||
extra={"spider": spider},
|
||||
)
|
||||
robotstxt_body = ""
|
||||
return robotstxt_body
|
||||
body_decoded = ""
|
||||
return body_decoded
|
||||
|
||||
|
||||
class RobotParser(metaclass=ABCMeta):
|
||||
|
|
@ -66,82 +69,80 @@ class RobotParser(metaclass=ABCMeta):
|
|||
|
||||
|
||||
class PythonRobotParser(RobotParser):
|
||||
def __init__(self, robotstxt_body, spider):
|
||||
def __init__(self, robotstxt_body: bytes, spider: Optional[Spider]):
|
||||
from urllib.robotparser import RobotFileParser
|
||||
|
||||
self.spider = spider
|
||||
robotstxt_body = decode_robotstxt(
|
||||
robotstxt_body, spider, to_native_str_type=True
|
||||
)
|
||||
self.rp = RobotFileParser()
|
||||
self.rp.parse(robotstxt_body.splitlines())
|
||||
self.spider: Optional[Spider] = spider
|
||||
body_decoded = decode_robotstxt(robotstxt_body, spider, to_native_str_type=True)
|
||||
self.rp: RobotFileParser = RobotFileParser()
|
||||
self.rp.parse(body_decoded.splitlines())
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, robotstxt_body):
|
||||
def from_crawler(cls, crawler: Crawler, robotstxt_body: bytes) -> Self:
|
||||
spider = None if not crawler else crawler.spider
|
||||
o = cls(robotstxt_body, spider)
|
||||
return o
|
||||
|
||||
def allowed(self, url, user_agent):
|
||||
def allowed(self, url: Union[str, bytes], user_agent: Union[str, bytes]) -> bool:
|
||||
user_agent = to_unicode(user_agent)
|
||||
url = to_unicode(url)
|
||||
return self.rp.can_fetch(user_agent, url)
|
||||
|
||||
|
||||
class ReppyRobotParser(RobotParser):
|
||||
def __init__(self, robotstxt_body, spider):
|
||||
def __init__(self, robotstxt_body: bytes, spider: Optional[Spider]):
|
||||
warn("ReppyRobotParser is deprecated.", ScrapyDeprecationWarning, stacklevel=2)
|
||||
from reppy.robots import Robots
|
||||
|
||||
self.spider = spider
|
||||
self.spider: Optional[Spider] = spider
|
||||
self.rp = Robots.parse("", robotstxt_body)
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, robotstxt_body):
|
||||
def from_crawler(cls, crawler: Crawler, robotstxt_body: bytes) -> Self:
|
||||
spider = None if not crawler else crawler.spider
|
||||
o = cls(robotstxt_body, spider)
|
||||
return o
|
||||
|
||||
def allowed(self, url, user_agent):
|
||||
def allowed(self, url: Union[str, bytes], user_agent: Union[str, bytes]) -> bool:
|
||||
return self.rp.allowed(url, user_agent)
|
||||
|
||||
|
||||
class RerpRobotParser(RobotParser):
|
||||
def __init__(self, robotstxt_body, spider):
|
||||
def __init__(self, robotstxt_body: bytes, spider: Optional[Spider]):
|
||||
from robotexclusionrulesparser import RobotExclusionRulesParser
|
||||
|
||||
self.spider = spider
|
||||
self.rp = RobotExclusionRulesParser()
|
||||
robotstxt_body = decode_robotstxt(robotstxt_body, spider)
|
||||
self.rp.parse(robotstxt_body)
|
||||
self.spider: Optional[Spider] = spider
|
||||
self.rp: RobotExclusionRulesParser = RobotExclusionRulesParser()
|
||||
body_decoded = decode_robotstxt(robotstxt_body, spider)
|
||||
self.rp.parse(body_decoded)
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, robotstxt_body):
|
||||
def from_crawler(cls, crawler: Crawler, robotstxt_body: bytes) -> Self:
|
||||
spider = None if not crawler else crawler.spider
|
||||
o = cls(robotstxt_body, spider)
|
||||
return o
|
||||
|
||||
def allowed(self, url, user_agent):
|
||||
def allowed(self, url: Union[str, bytes], user_agent: Union[str, bytes]) -> bool:
|
||||
user_agent = to_unicode(user_agent)
|
||||
url = to_unicode(url)
|
||||
return self.rp.is_allowed(user_agent, url)
|
||||
|
||||
|
||||
class ProtegoRobotParser(RobotParser):
|
||||
def __init__(self, robotstxt_body, spider):
|
||||
def __init__(self, robotstxt_body: bytes, spider: Optional[Spider]):
|
||||
from protego import Protego
|
||||
|
||||
self.spider = spider
|
||||
robotstxt_body = decode_robotstxt(robotstxt_body, spider)
|
||||
self.rp = Protego.parse(robotstxt_body)
|
||||
self.spider: Optional[Spider] = spider
|
||||
body_decoded = decode_robotstxt(robotstxt_body, spider)
|
||||
self.rp = Protego.parse(body_decoded)
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, robotstxt_body):
|
||||
def from_crawler(cls, crawler: Crawler, robotstxt_body: bytes) -> Self:
|
||||
spider = None if not crawler else crawler.spider
|
||||
o = cls(robotstxt_body, spider)
|
||||
return o
|
||||
|
||||
def allowed(self, url, user_agent):
|
||||
def allowed(self, url: Union[str, bytes], user_agent: Union[str, bytes]) -> bool:
|
||||
user_agent = to_unicode(user_agent)
|
||||
url = to_unicode(url)
|
||||
return self.rp.can_fetch(url, user_agent)
|
||||
|
|
|
|||
|
|
@ -2,20 +2,28 @@
|
|||
Scheduler queues
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import marshal
|
||||
import pickle # nosec
|
||||
from os import PathLike
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Type, Union
|
||||
|
||||
from queuelib import queue
|
||||
|
||||
from scrapy import Request
|
||||
from scrapy.crawler import Crawler
|
||||
from scrapy.utils.request import request_from_dict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# typing.Self requires Python 3.11
|
||||
from typing_extensions import Self
|
||||
|
||||
def _with_mkdir(queue_class):
|
||||
class DirectoriesCreated(queue_class):
|
||||
def __init__(self, path: Union[str, PathLike], *args, **kwargs):
|
||||
|
||||
def _with_mkdir(queue_class: Type[queue.BaseQueue]) -> Type[queue.BaseQueue]:
|
||||
class DirectoriesCreated(queue_class): # type: ignore[valid-type,misc]
|
||||
def __init__(self, path: Union[str, PathLike], *args: Any, **kwargs: Any):
|
||||
dirname = Path(path).parent
|
||||
if not dirname.exists():
|
||||
dirname.mkdir(parents=True, exist_ok=True)
|
||||
|
|
@ -24,18 +32,23 @@ def _with_mkdir(queue_class):
|
|||
return DirectoriesCreated
|
||||
|
||||
|
||||
def _serializable_queue(queue_class, serialize, deserialize):
|
||||
class SerializableQueue(queue_class):
|
||||
def push(self, obj):
|
||||
def _serializable_queue(
|
||||
queue_class: Type[queue.BaseQueue],
|
||||
serialize: Callable[[Any], bytes],
|
||||
deserialize: Callable[[bytes], Any],
|
||||
) -> Type[queue.BaseQueue]:
|
||||
class SerializableQueue(queue_class): # type: ignore[valid-type,misc]
|
||||
def push(self, obj: Any) -> None:
|
||||
s = serialize(obj)
|
||||
super().push(s)
|
||||
|
||||
def pop(self):
|
||||
def pop(self) -> Optional[Any]:
|
||||
s = super().pop()
|
||||
if s:
|
||||
return deserialize(s)
|
||||
return None
|
||||
|
||||
def peek(self):
|
||||
def peek(self) -> Optional[Any]:
|
||||
"""Returns the next object to be returned by :meth:`pop`,
|
||||
but without removing it from the queue.
|
||||
|
||||
|
|
@ -50,31 +63,36 @@ def _serializable_queue(queue_class, serialize, deserialize):
|
|||
) from ex
|
||||
if s:
|
||||
return deserialize(s)
|
||||
return None
|
||||
|
||||
return SerializableQueue
|
||||
|
||||
|
||||
def _scrapy_serialization_queue(queue_class):
|
||||
class ScrapyRequestQueue(queue_class):
|
||||
def __init__(self, crawler, key):
|
||||
def _scrapy_serialization_queue(
|
||||
queue_class: Type[queue.BaseQueue],
|
||||
) -> Type[queue.BaseQueue]:
|
||||
class ScrapyRequestQueue(queue_class): # type: ignore[valid-type,misc]
|
||||
def __init__(self, crawler: Crawler, key: str):
|
||||
self.spider = crawler.spider
|
||||
super().__init__(key)
|
||||
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, key, *args, **kwargs):
|
||||
def from_crawler(
|
||||
cls, crawler: Crawler, key: str, *args: Any, **kwargs: Any
|
||||
) -> Self:
|
||||
return cls(crawler, key)
|
||||
|
||||
def push(self, request):
|
||||
request = request.to_dict(spider=self.spider)
|
||||
return super().push(request)
|
||||
def push(self, request: Request) -> None:
|
||||
request_dict = request.to_dict(spider=self.spider)
|
||||
super().push(request_dict)
|
||||
|
||||
def pop(self):
|
||||
def pop(self) -> Optional[Request]:
|
||||
request = super().pop()
|
||||
if not request:
|
||||
return None
|
||||
return request_from_dict(request, spider=self.spider)
|
||||
|
||||
def peek(self):
|
||||
def peek(self) -> Optional[Request]:
|
||||
"""Returns the next object to be returned by :meth:`pop`,
|
||||
but without removing it from the queue.
|
||||
|
||||
|
|
@ -89,13 +107,15 @@ def _scrapy_serialization_queue(queue_class):
|
|||
return ScrapyRequestQueue
|
||||
|
||||
|
||||
def _scrapy_non_serialization_queue(queue_class):
|
||||
class ScrapyRequestQueue(queue_class):
|
||||
def _scrapy_non_serialization_queue(
|
||||
queue_class: Type[queue.BaseQueue],
|
||||
) -> Type[queue.BaseQueue]:
|
||||
class ScrapyRequestQueue(queue_class): # type: ignore[valid-type,misc]
|
||||
@classmethod
|
||||
def from_crawler(cls, crawler, *args, **kwargs):
|
||||
def from_crawler(cls, crawler: Crawler, *args: Any, **kwargs: Any) -> Self:
|
||||
return cls()
|
||||
|
||||
def peek(self):
|
||||
def peek(self) -> Optional[Any]:
|
||||
"""Returns the next object to be returned by :meth:`pop`,
|
||||
but without removing it from the queue.
|
||||
|
||||
|
|
@ -113,7 +133,7 @@ def _scrapy_non_serialization_queue(queue_class):
|
|||
return ScrapyRequestQueue
|
||||
|
||||
|
||||
def _pickle_serialize(obj):
|
||||
def _pickle_serialize(obj: Any) -> bytes:
|
||||
try:
|
||||
return pickle.dumps(obj, protocol=4)
|
||||
# Both pickle.PicklingError and AttributeError can be raised by pickle.dump(s)
|
||||
|
|
@ -122,17 +142,18 @@ def _pickle_serialize(obj):
|
|||
raise ValueError(str(e)) from e
|
||||
|
||||
|
||||
# queue.*Queue aren't subclasses of queue.BaseQueue
|
||||
_PickleFifoSerializationDiskQueue = _serializable_queue(
|
||||
_with_mkdir(queue.FifoDiskQueue), _pickle_serialize, pickle.loads
|
||||
_with_mkdir(queue.FifoDiskQueue), _pickle_serialize, pickle.loads # type: ignore[arg-type]
|
||||
)
|
||||
_PickleLifoSerializationDiskQueue = _serializable_queue(
|
||||
_with_mkdir(queue.LifoDiskQueue), _pickle_serialize, pickle.loads
|
||||
_with_mkdir(queue.LifoDiskQueue), _pickle_serialize, pickle.loads # type: ignore[arg-type]
|
||||
)
|
||||
_MarshalFifoSerializationDiskQueue = _serializable_queue(
|
||||
_with_mkdir(queue.FifoDiskQueue), marshal.dumps, marshal.loads
|
||||
_with_mkdir(queue.FifoDiskQueue), marshal.dumps, marshal.loads # type: ignore[arg-type]
|
||||
)
|
||||
_MarshalLifoSerializationDiskQueue = _serializable_queue(
|
||||
_with_mkdir(queue.LifoDiskQueue), marshal.dumps, marshal.loads
|
||||
_with_mkdir(queue.LifoDiskQueue), marshal.dumps, marshal.loads # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
# public queue classes
|
||||
|
|
@ -140,5 +161,5 @@ PickleFifoDiskQueue = _scrapy_serialization_queue(_PickleFifoSerializationDiskQu
|
|||
PickleLifoDiskQueue = _scrapy_serialization_queue(_PickleLifoSerializationDiskQueue)
|
||||
MarshalFifoDiskQueue = _scrapy_serialization_queue(_MarshalFifoSerializationDiskQueue)
|
||||
MarshalLifoDiskQueue = _scrapy_serialization_queue(_MarshalLifoSerializationDiskQueue)
|
||||
FifoMemoryQueue = _scrapy_non_serialization_queue(queue.FifoMemoryQueue)
|
||||
LifoMemoryQueue = _scrapy_non_serialization_queue(queue.LifoMemoryQueue)
|
||||
FifoMemoryQueue = _scrapy_non_serialization_queue(queue.FifoMemoryQueue) # type: ignore[arg-type]
|
||||
LifoMemoryQueue = _scrapy_non_serialization_queue(queue.LifoMemoryQueue) # type: ignore[arg-type]
|
||||
|
|
|
|||
Loading…
Reference in New Issue