web_request.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. import asyncio
  2. import datetime
  3. import io
  4. import re
  5. import socket
  6. import string
  7. import tempfile
  8. import types
  9. import warnings
  10. from email.utils import parsedate
  11. from http.cookies import SimpleCookie
  12. from types import MappingProxyType
  13. from typing import (
  14. TYPE_CHECKING,
  15. Any,
  16. Dict,
  17. Iterator,
  18. Mapping,
  19. MutableMapping,
  20. Optional,
  21. Tuple,
  22. Union,
  23. cast,
  24. )
  25. from urllib.parse import parse_qsl
  26. import attr
  27. from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
  28. from yarl import URL
  29. from . import hdrs
  30. from .abc import AbstractStreamWriter
  31. from .helpers import DEBUG, ChainMapProxy, HeadersMixin, reify, sentinel
  32. from .http_parser import RawRequestMessage
  33. from .http_writer import HttpVersion
  34. from .multipart import BodyPartReader, MultipartReader
  35. from .streams import EmptyStreamReader, StreamReader
  36. from .typedefs import (
  37. DEFAULT_JSON_DECODER,
  38. JSONDecoder,
  39. LooseHeaders,
  40. RawHeaders,
  41. StrOrURL,
  42. )
  43. from .web_exceptions import HTTPRequestEntityTooLarge
  44. from .web_response import StreamResponse
  45. __all__ = ("BaseRequest", "FileField", "Request")
  46. if TYPE_CHECKING: # pragma: no cover
  47. from .web_app import Application
  48. from .web_protocol import RequestHandler
  49. from .web_urldispatcher import UrlMappingMatchInfo
  50. @attr.s(auto_attribs=True, frozen=True, slots=True)
  51. class FileField:
  52. name: str
  53. filename: str
  54. file: io.BufferedReader
  55. content_type: str
  56. headers: "CIMultiDictProxy[str]"
  57. _TCHAR = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
  58. # '-' at the end to prevent interpretation as range in a char class
  59. _TOKEN = fr"[{_TCHAR}]+"
  60. _QDTEXT = r"[{}]".format(
  61. r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F)))
  62. )
  63. # qdtext includes 0x5C to escape 0x5D ('\]')
  64. # qdtext excludes obs-text (because obsoleted, and encoding not specified)
  65. _QUOTED_PAIR = r"\\[\t !-~]"
  66. _QUOTED_STRING = r'"(?:{quoted_pair}|{qdtext})*"'.format(
  67. qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR
  68. )
  69. _FORWARDED_PAIR = r"({token})=({token}|{quoted_string})(:\d{{1,4}})?".format(
  70. token=_TOKEN, quoted_string=_QUOTED_STRING
  71. )
  72. _QUOTED_PAIR_REPLACE_RE = re.compile(r"\\([\t !-~])")
  73. # same pattern as _QUOTED_PAIR but contains a capture group
  74. _FORWARDED_PAIR_RE = re.compile(_FORWARDED_PAIR)
  75. ############################################################
  76. # HTTP Request
  77. ############################################################
  78. class BaseRequest(MutableMapping[str, Any], HeadersMixin):
  79. POST_METHODS = {
  80. hdrs.METH_PATCH,
  81. hdrs.METH_POST,
  82. hdrs.METH_PUT,
  83. hdrs.METH_TRACE,
  84. hdrs.METH_DELETE,
  85. }
  86. ATTRS = HeadersMixin.ATTRS | frozenset(
  87. [
  88. "_message",
  89. "_protocol",
  90. "_payload_writer",
  91. "_payload",
  92. "_headers",
  93. "_method",
  94. "_version",
  95. "_rel_url",
  96. "_post",
  97. "_read_bytes",
  98. "_state",
  99. "_cache",
  100. "_task",
  101. "_client_max_size",
  102. "_loop",
  103. "_transport_sslcontext",
  104. "_transport_peername",
  105. ]
  106. )
  107. def __init__(
  108. self,
  109. message: RawRequestMessage,
  110. payload: StreamReader,
  111. protocol: "RequestHandler",
  112. payload_writer: AbstractStreamWriter,
  113. task: "asyncio.Task[None]",
  114. loop: asyncio.AbstractEventLoop,
  115. *,
  116. client_max_size: int = 1024 ** 2,
  117. state: Optional[Dict[str, Any]] = None,
  118. scheme: Optional[str] = None,
  119. host: Optional[str] = None,
  120. remote: Optional[str] = None,
  121. ) -> None:
  122. if state is None:
  123. state = {}
  124. self._message = message
  125. self._protocol = protocol
  126. self._payload_writer = payload_writer
  127. self._payload = payload
  128. self._headers = message.headers
  129. self._method = message.method
  130. self._version = message.version
  131. self._rel_url = message.url
  132. self._post = (
  133. None
  134. ) # type: Optional[MultiDictProxy[Union[str, bytes, FileField]]]
  135. self._read_bytes = None # type: Optional[bytes]
  136. self._state = state
  137. self._cache = {} # type: Dict[str, Any]
  138. self._task = task
  139. self._client_max_size = client_max_size
  140. self._loop = loop
  141. transport = self._protocol.transport
  142. assert transport is not None
  143. self._transport_sslcontext = transport.get_extra_info("sslcontext")
  144. self._transport_peername = transport.get_extra_info("peername")
  145. if scheme is not None:
  146. self._cache["scheme"] = scheme
  147. if host is not None:
  148. self._cache["host"] = host
  149. if remote is not None:
  150. self._cache["remote"] = remote
  151. def clone(
  152. self,
  153. *,
  154. method: str = sentinel,
  155. rel_url: StrOrURL = sentinel,
  156. headers: LooseHeaders = sentinel,
  157. scheme: str = sentinel,
  158. host: str = sentinel,
  159. remote: str = sentinel,
  160. ) -> "BaseRequest":
  161. """Clone itself with replacement some attributes.
  162. Creates and returns a new instance of Request object. If no parameters
  163. are given, an exact copy is returned. If a parameter is not passed, it
  164. will reuse the one from the current request object.
  165. """
  166. if self._read_bytes:
  167. raise RuntimeError("Cannot clone request " "after reading its content")
  168. dct = {} # type: Dict[str, Any]
  169. if method is not sentinel:
  170. dct["method"] = method
  171. if rel_url is not sentinel:
  172. new_url = URL(rel_url)
  173. dct["url"] = new_url
  174. dct["path"] = str(new_url)
  175. if headers is not sentinel:
  176. # a copy semantic
  177. dct["headers"] = CIMultiDictProxy(CIMultiDict(headers))
  178. dct["raw_headers"] = tuple(
  179. (k.encode("utf-8"), v.encode("utf-8")) for k, v in headers.items()
  180. )
  181. message = self._message._replace(**dct)
  182. kwargs = {}
  183. if scheme is not sentinel:
  184. kwargs["scheme"] = scheme
  185. if host is not sentinel:
  186. kwargs["host"] = host
  187. if remote is not sentinel:
  188. kwargs["remote"] = remote
  189. return self.__class__(
  190. message,
  191. self._payload,
  192. self._protocol,
  193. self._payload_writer,
  194. self._task,
  195. self._loop,
  196. client_max_size=self._client_max_size,
  197. state=self._state.copy(),
  198. **kwargs,
  199. )
  200. @property
  201. def task(self) -> "asyncio.Task[None]":
  202. return self._task
  203. @property
  204. def protocol(self) -> "RequestHandler":
  205. return self._protocol
  206. @property
  207. def transport(self) -> Optional[asyncio.Transport]:
  208. if self._protocol is None:
  209. return None
  210. return self._protocol.transport
  211. @property
  212. def writer(self) -> AbstractStreamWriter:
  213. return self._payload_writer
  214. @reify
  215. def message(self) -> RawRequestMessage:
  216. warnings.warn("Request.message is deprecated", DeprecationWarning, stacklevel=3)
  217. return self._message
  218. @reify
  219. def rel_url(self) -> URL:
  220. return self._rel_url
  221. @reify
  222. def loop(self) -> asyncio.AbstractEventLoop:
  223. warnings.warn(
  224. "request.loop property is deprecated", DeprecationWarning, stacklevel=2
  225. )
  226. return self._loop
  227. # MutableMapping API
  228. def __getitem__(self, key: str) -> Any:
  229. return self._state[key]
  230. def __setitem__(self, key: str, value: Any) -> None:
  231. self._state[key] = value
  232. def __delitem__(self, key: str) -> None:
  233. del self._state[key]
  234. def __len__(self) -> int:
  235. return len(self._state)
  236. def __iter__(self) -> Iterator[str]:
  237. return iter(self._state)
  238. ########
  239. @reify
  240. def secure(self) -> bool:
  241. """A bool indicating if the request is handled with SSL."""
  242. return self.scheme == "https"
  243. @reify
  244. def forwarded(self) -> Tuple[Mapping[str, str], ...]:
  245. """A tuple containing all parsed Forwarded header(s).
  246. Makes an effort to parse Forwarded headers as specified by RFC 7239:
  247. - It adds one (immutable) dictionary per Forwarded 'field-value', ie
  248. per proxy. The element corresponds to the data in the Forwarded
  249. field-value added by the first proxy encountered by the client. Each
  250. subsequent item corresponds to those added by later proxies.
  251. - It checks that every value has valid syntax in general as specified
  252. in section 4: either a 'token' or a 'quoted-string'.
  253. - It un-escapes found escape sequences.
  254. - It does NOT validate 'by' and 'for' contents as specified in section
  255. 6.
  256. - It does NOT validate 'host' contents (Host ABNF).
  257. - It does NOT validate 'proto' contents for valid URI scheme names.
  258. Returns a tuple containing one or more immutable dicts
  259. """
  260. elems = []
  261. for field_value in self._message.headers.getall(hdrs.FORWARDED, ()):
  262. length = len(field_value)
  263. pos = 0
  264. need_separator = False
  265. elem = {} # type: Dict[str, str]
  266. elems.append(types.MappingProxyType(elem))
  267. while 0 <= pos < length:
  268. match = _FORWARDED_PAIR_RE.match(field_value, pos)
  269. if match is not None: # got a valid forwarded-pair
  270. if need_separator:
  271. # bad syntax here, skip to next comma
  272. pos = field_value.find(",", pos)
  273. else:
  274. name, value, port = match.groups()
  275. if value[0] == '"':
  276. # quoted string: remove quotes and unescape
  277. value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1])
  278. if port:
  279. value += port
  280. elem[name.lower()] = value
  281. pos += len(match.group(0))
  282. need_separator = True
  283. elif field_value[pos] == ",": # next forwarded-element
  284. need_separator = False
  285. elem = {}
  286. elems.append(types.MappingProxyType(elem))
  287. pos += 1
  288. elif field_value[pos] == ";": # next forwarded-pair
  289. need_separator = False
  290. pos += 1
  291. elif field_value[pos] in " \t":
  292. # Allow whitespace even between forwarded-pairs, though
  293. # RFC 7239 doesn't. This simplifies code and is in line
  294. # with Postel's law.
  295. pos += 1
  296. else:
  297. # bad syntax here, skip to next comma
  298. pos = field_value.find(",", pos)
  299. return tuple(elems)
  300. @reify
  301. def scheme(self) -> str:
  302. """A string representing the scheme of the request.
  303. Hostname is resolved in this order:
  304. - overridden value by .clone(scheme=new_scheme) call.
  305. - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise.
  306. 'http' or 'https'.
  307. """
  308. if self._transport_sslcontext:
  309. return "https"
  310. else:
  311. return "http"
  312. @reify
  313. def method(self) -> str:
  314. """Read only property for getting HTTP method.
  315. The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
  316. """
  317. return self._method
  318. @reify
  319. def version(self) -> HttpVersion:
  320. """Read only property for getting HTTP version of request.
  321. Returns aiohttp.protocol.HttpVersion instance.
  322. """
  323. return self._version
  324. @reify
  325. def host(self) -> str:
  326. """Hostname of the request.
  327. Hostname is resolved in this order:
  328. - overridden value by .clone(host=new_host) call.
  329. - HOST HTTP header
  330. - socket.getfqdn() value
  331. """
  332. host = self._message.headers.get(hdrs.HOST)
  333. if host is not None:
  334. return host
  335. else:
  336. return socket.getfqdn()
  337. @reify
  338. def remote(self) -> Optional[str]:
  339. """Remote IP of client initiated HTTP request.
  340. The IP is resolved in this order:
  341. - overridden value by .clone(remote=new_remote) call.
  342. - peername of opened socket
  343. """
  344. if isinstance(self._transport_peername, (list, tuple)):
  345. return self._transport_peername[0]
  346. else:
  347. return self._transport_peername
  348. @reify
  349. def url(self) -> URL:
  350. url = URL.build(scheme=self.scheme, host=self.host)
  351. return url.join(self._rel_url)
  352. @reify
  353. def path(self) -> str:
  354. """The URL including *PATH INFO* without the host or scheme.
  355. E.g., ``/app/blog``
  356. """
  357. return self._rel_url.path
  358. @reify
  359. def path_qs(self) -> str:
  360. """The URL including PATH_INFO and the query string.
  361. E.g, /app/blog?id=10
  362. """
  363. return str(self._rel_url)
  364. @reify
  365. def raw_path(self) -> str:
  366. """The URL including raw *PATH INFO* without the host or scheme.
  367. Warning, the path is unquoted and may contains non valid URL characters
  368. E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters``
  369. """
  370. return self._message.path
  371. @reify
  372. def query(self) -> "MultiDictProxy[str]":
  373. """A multidict with all the variables in the query string."""
  374. return self._rel_url.query
  375. @reify
  376. def query_string(self) -> str:
  377. """The query string in the URL.
  378. E.g., id=10
  379. """
  380. return self._rel_url.query_string
  381. @reify
  382. def headers(self) -> "CIMultiDictProxy[str]":
  383. """A case-insensitive multidict proxy with all headers."""
  384. return self._headers
  385. @reify
  386. def raw_headers(self) -> RawHeaders:
  387. """A sequence of pairs for all headers."""
  388. return self._message.raw_headers
  389. @staticmethod
  390. def _http_date(_date_str: Optional[str]) -> Optional[datetime.datetime]:
  391. """Process a date string, return a datetime object"""
  392. if _date_str is not None:
  393. timetuple = parsedate(_date_str)
  394. if timetuple is not None:
  395. return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc)
  396. return None
  397. @reify
  398. def if_modified_since(self) -> Optional[datetime.datetime]:
  399. """The value of If-Modified-Since HTTP header, or None.
  400. This header is represented as a `datetime` object.
  401. """
  402. return self._http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE))
  403. @reify
  404. def if_unmodified_since(self) -> Optional[datetime.datetime]:
  405. """The value of If-Unmodified-Since HTTP header, or None.
  406. This header is represented as a `datetime` object.
  407. """
  408. return self._http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE))
  409. @reify
  410. def if_range(self) -> Optional[datetime.datetime]:
  411. """The value of If-Range HTTP header, or None.
  412. This header is represented as a `datetime` object.
  413. """
  414. return self._http_date(self.headers.get(hdrs.IF_RANGE))
  415. @reify
  416. def keep_alive(self) -> bool:
  417. """Is keepalive enabled by client?"""
  418. return not self._message.should_close
  419. @reify
  420. def cookies(self) -> Mapping[str, str]:
  421. """Return request cookies.
  422. A read-only dictionary-like object.
  423. """
  424. raw = self.headers.get(hdrs.COOKIE, "")
  425. parsed = SimpleCookie(raw) # type: SimpleCookie[str]
  426. return MappingProxyType({key: val.value for key, val in parsed.items()})
  427. @reify
  428. def http_range(self) -> slice:
  429. """The content of Range HTTP header.
  430. Return a slice instance.
  431. """
  432. rng = self._headers.get(hdrs.RANGE)
  433. start, end = None, None
  434. if rng is not None:
  435. try:
  436. pattern = r"^bytes=(\d*)-(\d*)$"
  437. start, end = re.findall(pattern, rng)[0]
  438. except IndexError: # pattern was not found in header
  439. raise ValueError("range not in acceptable format")
  440. end = int(end) if end else None
  441. start = int(start) if start else None
  442. if start is None and end is not None:
  443. # end with no start is to return tail of content
  444. start = -end
  445. end = None
  446. if start is not None and end is not None:
  447. # end is inclusive in range header, exclusive for slice
  448. end += 1
  449. if start >= end:
  450. raise ValueError("start cannot be after end")
  451. if start is end is None: # No valid range supplied
  452. raise ValueError("No start or end of range specified")
  453. return slice(start, end, 1)
  454. @reify
  455. def content(self) -> StreamReader:
  456. """Return raw payload stream."""
  457. return self._payload
  458. @property
  459. def has_body(self) -> bool:
  460. """Return True if request's HTTP BODY can be read, False otherwise."""
  461. warnings.warn(
  462. "Deprecated, use .can_read_body #2005", DeprecationWarning, stacklevel=2
  463. )
  464. return not self._payload.at_eof()
  465. @property
  466. def can_read_body(self) -> bool:
  467. """Return True if request's HTTP BODY can be read, False otherwise."""
  468. return not self._payload.at_eof()
  469. @reify
  470. def body_exists(self) -> bool:
  471. """Return True if request has HTTP BODY, False otherwise."""
  472. return type(self._payload) is not EmptyStreamReader
  473. async def release(self) -> None:
  474. """Release request.
  475. Eat unread part of HTTP BODY if present.
  476. """
  477. while not self._payload.at_eof():
  478. await self._payload.readany()
  479. async def read(self) -> bytes:
  480. """Read request body if present.
  481. Returns bytes object with full request content.
  482. """
  483. if self._read_bytes is None:
  484. body = bytearray()
  485. while True:
  486. chunk = await self._payload.readany()
  487. body.extend(chunk)
  488. if self._client_max_size:
  489. body_size = len(body)
  490. if body_size >= self._client_max_size:
  491. raise HTTPRequestEntityTooLarge(
  492. max_size=self._client_max_size, actual_size=body_size
  493. )
  494. if not chunk:
  495. break
  496. self._read_bytes = bytes(body)
  497. return self._read_bytes
  498. async def text(self) -> str:
  499. """Return BODY as text using encoding from .charset."""
  500. bytes_body = await self.read()
  501. encoding = self.charset or "utf-8"
  502. return bytes_body.decode(encoding)
  503. async def json(self, *, loads: JSONDecoder = DEFAULT_JSON_DECODER) -> Any:
  504. """Return BODY as JSON."""
  505. body = await self.text()
  506. return loads(body)
  507. async def multipart(self) -> MultipartReader:
  508. """Return async iterator to process BODY as multipart."""
  509. return MultipartReader(self._headers, self._payload)
  510. async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]":
  511. """Return POST parameters."""
  512. if self._post is not None:
  513. return self._post
  514. if self._method not in self.POST_METHODS:
  515. self._post = MultiDictProxy(MultiDict())
  516. return self._post
  517. content_type = self.content_type
  518. if content_type not in (
  519. "",
  520. "application/x-www-form-urlencoded",
  521. "multipart/form-data",
  522. ):
  523. self._post = MultiDictProxy(MultiDict())
  524. return self._post
  525. out = MultiDict() # type: MultiDict[Union[str, bytes, FileField]]
  526. if content_type == "multipart/form-data":
  527. multipart = await self.multipart()
  528. max_size = self._client_max_size
  529. field = await multipart.next()
  530. while field is not None:
  531. size = 0
  532. field_ct = field.headers.get(hdrs.CONTENT_TYPE)
  533. if isinstance(field, BodyPartReader):
  534. assert field.name is not None
  535. # Note that according to RFC 7578, the Content-Type header
  536. # is optional, even for files, so we can't assume it's
  537. # present.
  538. # https://tools.ietf.org/html/rfc7578#section-4.4
  539. if field.filename:
  540. # store file in temp file
  541. tmp = tempfile.TemporaryFile()
  542. chunk = await field.read_chunk(size=2 ** 16)
  543. while chunk:
  544. chunk = field.decode(chunk)
  545. tmp.write(chunk)
  546. size += len(chunk)
  547. if 0 < max_size < size:
  548. raise HTTPRequestEntityTooLarge(
  549. max_size=max_size, actual_size=size
  550. )
  551. chunk = await field.read_chunk(size=2 ** 16)
  552. tmp.seek(0)
  553. if field_ct is None:
  554. field_ct = "application/octet-stream"
  555. ff = FileField(
  556. field.name,
  557. field.filename,
  558. cast(io.BufferedReader, tmp),
  559. field_ct,
  560. field.headers,
  561. )
  562. out.add(field.name, ff)
  563. else:
  564. # deal with ordinary data
  565. value = await field.read(decode=True)
  566. if field_ct is None or field_ct.startswith("text/"):
  567. charset = field.get_charset(default="utf-8")
  568. out.add(field.name, value.decode(charset))
  569. else:
  570. out.add(field.name, value)
  571. size += len(value)
  572. if 0 < max_size < size:
  573. raise HTTPRequestEntityTooLarge(
  574. max_size=max_size, actual_size=size
  575. )
  576. else:
  577. raise ValueError(
  578. "To decode nested multipart you need " "to use custom reader",
  579. )
  580. field = await multipart.next()
  581. else:
  582. data = await self.read()
  583. if data:
  584. charset = self.charset or "utf-8"
  585. out.extend(
  586. parse_qsl(
  587. data.rstrip().decode(charset),
  588. keep_blank_values=True,
  589. encoding=charset,
  590. )
  591. )
  592. self._post = MultiDictProxy(out)
  593. return self._post
  594. def get_extra_info(self, name: str, default: Any = None) -> Any:
  595. """Extra info from protocol transport"""
  596. protocol = self._protocol
  597. if protocol is None:
  598. return default
  599. transport = protocol.transport
  600. if transport is None:
  601. return default
  602. return transport.get_extra_info(name, default)
  603. def __repr__(self) -> str:
  604. ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode(
  605. "ascii"
  606. )
  607. return "<{} {} {} >".format(
  608. self.__class__.__name__, self._method, ascii_encodable_path
  609. )
  610. def __eq__(self, other: object) -> bool:
  611. return id(self) == id(other)
  612. def __bool__(self) -> bool:
  613. return True
  614. async def _prepare_hook(self, response: StreamResponse) -> None:
  615. return
  616. def _cancel(self, exc: BaseException) -> None:
  617. self._payload.set_exception(exc)
  618. class Request(BaseRequest):
  619. ATTRS = BaseRequest.ATTRS | frozenset(["_match_info"])
  620. def __init__(self, *args: Any, **kwargs: Any) -> None:
  621. super().__init__(*args, **kwargs)
  622. # matchdict, route_name, handler
  623. # or information about traversal lookup
  624. # initialized after route resolving
  625. self._match_info = None # type: Optional[UrlMappingMatchInfo]
  626. if DEBUG:
  627. def __setattr__(self, name: str, val: Any) -> None:
  628. if name not in self.ATTRS:
  629. warnings.warn(
  630. "Setting custom {}.{} attribute "
  631. "is discouraged".format(self.__class__.__name__, name),
  632. DeprecationWarning,
  633. stacklevel=2,
  634. )
  635. super().__setattr__(name, val)
  636. def clone(
  637. self,
  638. *,
  639. method: str = sentinel,
  640. rel_url: StrOrURL = sentinel,
  641. headers: LooseHeaders = sentinel,
  642. scheme: str = sentinel,
  643. host: str = sentinel,
  644. remote: str = sentinel,
  645. ) -> "Request":
  646. ret = super().clone(
  647. method=method,
  648. rel_url=rel_url,
  649. headers=headers,
  650. scheme=scheme,
  651. host=host,
  652. remote=remote,
  653. )
  654. new_ret = cast(Request, ret)
  655. new_ret._match_info = self._match_info
  656. return new_ret
  657. @reify
  658. def match_info(self) -> "UrlMappingMatchInfo":
  659. """Result of route resolving."""
  660. match_info = self._match_info
  661. assert match_info is not None
  662. return match_info
  663. @property
  664. def app(self) -> "Application":
  665. """Application instance."""
  666. match_info = self._match_info
  667. assert match_info is not None
  668. return match_info.current_app
  669. @property
  670. def config_dict(self) -> ChainMapProxy:
  671. match_info = self._match_info
  672. assert match_info is not None
  673. lst = match_info.apps
  674. app = self.app
  675. idx = lst.index(app)
  676. sublist = list(reversed(lst[: idx + 1]))
  677. return ChainMapProxy(sublist)
  678. async def _prepare_hook(self, response: StreamResponse) -> None:
  679. match_info = self._match_info
  680. if match_info is None:
  681. return
  682. for app in match_info._apps:
  683. await app.on_response_prepare.send(self, response)