Coverage for /pythoncovmergedfiles/medio/medio/src/aiohttp/aiohttp/web_request.py: 41%
450 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:52 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:52 +0000
1import asyncio
2import dataclasses
3import datetime
4import io
5import re
6import socket
7import string
8import tempfile
9import types
10from http.cookies import SimpleCookie
11from types import MappingProxyType
12from typing import (
13 TYPE_CHECKING,
14 Any,
15 Dict,
16 Iterator,
17 Mapping,
18 MutableMapping,
19 Optional,
20 Pattern,
21 Set,
22 Tuple,
23 Union,
24 cast,
25)
26from urllib.parse import parse_qsl
28from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
29from typing_extensions import Final
30from yarl import URL
32from . import hdrs
33from .abc import AbstractStreamWriter
34from .helpers import (
35 _SENTINEL,
36 ETAG_ANY,
37 LIST_QUOTED_ETAG_RE,
38 ChainMapProxy,
39 ETag,
40 HeadersMixin,
41 is_expected_content_type,
42 parse_http_date,
43 reify,
44 sentinel,
45 set_result,
46)
47from .http_parser import RawRequestMessage
48from .http_writer import HttpVersion
49from .multipart import BodyPartReader, MultipartReader
50from .streams import EmptyStreamReader, StreamReader
51from .typedefs import (
52 DEFAULT_JSON_DECODER,
53 JSONDecoder,
54 LooseHeaders,
55 RawHeaders,
56 StrOrURL,
57)
58from .web_exceptions import (
59 HTTPBadRequest,
60 HTTPRequestEntityTooLarge,
61 HTTPUnsupportedMediaType,
62)
63from .web_response import StreamResponse
65__all__ = ("BaseRequest", "FileField", "Request")
68if TYPE_CHECKING: # pragma: no cover
69 from .web_app import Application
70 from .web_protocol import RequestHandler
71 from .web_urldispatcher import UrlMappingMatchInfo
74@dataclasses.dataclass(frozen=True)
75class FileField:
76 name: str
77 filename: str
78 file: io.BufferedReader
79 content_type: str
80 headers: "CIMultiDictProxy[str]"
83_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
84# '-' at the end to prevent interpretation as range in a char class
86_TOKEN: Final[str] = rf"[{_TCHAR}]+"
88_QDTEXT: Final[str] = r"[{}]".format(
89 r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F)))
90)
91# qdtext includes 0x5C to escape 0x5D ('\]')
92# qdtext excludes obs-text (because obsoleted, and encoding not specified)
94_QUOTED_PAIR: Final[str] = r"\\[\t !-~]"
96_QUOTED_STRING: Final[str] = r'"(?:{quoted_pair}|{qdtext})*"'.format(
97 qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR
98)
100_FORWARDED_PAIR: Final[
101 str
102] = r"({token})=({token}|{quoted_string})(:\d{{1,4}})?".format(
103 token=_TOKEN, quoted_string=_QUOTED_STRING
104)
106_QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])")
107# same pattern as _QUOTED_PAIR but contains a capture group
109_FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR)
111############################################################
112# HTTP Request
113############################################################
116class BaseRequest(MutableMapping[str, Any], HeadersMixin):
117 POST_METHODS = {
118 hdrs.METH_PATCH,
119 hdrs.METH_POST,
120 hdrs.METH_PUT,
121 hdrs.METH_TRACE,
122 hdrs.METH_DELETE,
123 }
125 __slots__ = (
126 "_message",
127 "_protocol",
128 "_payload_writer",
129 "_payload",
130 "_headers",
131 "_method",
132 "_version",
133 "_rel_url",
134 "_post",
135 "_read_bytes",
136 "_state",
137 "_cache",
138 "_task",
139 "_client_max_size",
140 "_loop",
141 "_transport_sslcontext",
142 "_transport_peername",
143 "_disconnection_waiters",
144 "__weakref__",
145 )
147 def __init__(
148 self,
149 message: RawRequestMessage,
150 payload: StreamReader,
151 protocol: "RequestHandler",
152 payload_writer: AbstractStreamWriter,
153 task: "asyncio.Task[None]",
154 loop: asyncio.AbstractEventLoop,
155 *,
156 client_max_size: int = 1024**2,
157 state: Optional[Dict[str, Any]] = None,
158 scheme: Optional[str] = None,
159 host: Optional[str] = None,
160 remote: Optional[str] = None,
161 ) -> None:
162 super().__init__()
163 if state is None:
164 state = {}
165 self._message = message
166 self._protocol = protocol
167 self._payload_writer = payload_writer
169 self._payload = payload
170 self._headers = message.headers
171 self._method = message.method
172 self._version = message.version
173 self._cache: Dict[str, Any] = {}
174 url = message.url
175 if url.is_absolute():
176 # absolute URL is given,
177 # override auto-calculating url, host, and scheme
178 # all other properties should be good
179 self._cache["url"] = url
180 self._cache["host"] = url.host
181 self._cache["scheme"] = url.scheme
182 self._rel_url = url.relative()
183 else:
184 self._rel_url = message.url
185 self._post: Optional[MultiDictProxy[Union[str, bytes, FileField]]] = None
186 self._read_bytes: Optional[bytes] = None
188 self._state = state
189 self._task = task
190 self._client_max_size = client_max_size
191 self._loop = loop
192 self._disconnection_waiters: Set[asyncio.Future[None]] = set()
194 transport = self._protocol.transport
195 assert transport is not None
196 self._transport_sslcontext = transport.get_extra_info("sslcontext")
197 self._transport_peername = transport.get_extra_info("peername")
199 if scheme is not None:
200 self._cache["scheme"] = scheme
201 if host is not None:
202 self._cache["host"] = host
203 if remote is not None:
204 self._cache["remote"] = remote
206 def clone(
207 self,
208 *,
209 method: Union[str, _SENTINEL] = sentinel,
210 rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
211 headers: Union[LooseHeaders, _SENTINEL] = sentinel,
212 scheme: Union[str, _SENTINEL] = sentinel,
213 host: Union[str, _SENTINEL] = sentinel,
214 remote: Union[str, _SENTINEL] = sentinel,
215 client_max_size: Union[int, _SENTINEL] = sentinel,
216 ) -> "BaseRequest":
217 """Clone itself with replacement some attributes.
219 Creates and returns a new instance of Request object. If no parameters
220 are given, an exact copy is returned. If a parameter is not passed, it
221 will reuse the one from the current request object.
222 """
223 if self._read_bytes:
224 raise RuntimeError("Cannot clone request " "after reading its content")
226 dct: Dict[str, Any] = {}
227 if method is not sentinel:
228 dct["method"] = method
229 if rel_url is not sentinel:
230 new_url: URL = URL(rel_url)
231 dct["url"] = new_url
232 dct["path"] = str(new_url)
233 if headers is not sentinel:
234 # a copy semantic
235 new_headers = CIMultiDictProxy(CIMultiDict(headers))
236 dct["headers"] = new_headers
237 dct["raw_headers"] = tuple(
238 (k.encode("utf-8"), v.encode("utf-8")) for k, v in new_headers.items()
239 )
241 message = self._message._replace(**dct)
243 kwargs: Dict[str, str] = {}
244 if scheme is not sentinel:
245 kwargs["scheme"] = scheme
246 if host is not sentinel:
247 kwargs["host"] = host
248 if remote is not sentinel:
249 kwargs["remote"] = remote
250 if client_max_size is sentinel:
251 client_max_size = self._client_max_size
253 return self.__class__(
254 message,
255 self._payload,
256 self._protocol,
257 self._payload_writer,
258 self._task,
259 self._loop,
260 client_max_size=client_max_size,
261 state=self._state.copy(),
262 **kwargs,
263 )
265 @property
266 def task(self) -> "asyncio.Task[None]":
267 return self._task
269 @property
270 def protocol(self) -> "RequestHandler":
271 return self._protocol
273 @property
274 def transport(self) -> Optional[asyncio.Transport]:
275 if self._protocol is None:
276 return None
277 return self._protocol.transport
279 @property
280 def writer(self) -> AbstractStreamWriter:
281 return self._payload_writer
283 @property
284 def client_max_size(self) -> int:
285 return self._client_max_size
287 @reify
288 def rel_url(self) -> URL:
289 return self._rel_url
291 # MutableMapping API
293 def __getitem__(self, key: str) -> Any:
294 return self._state[key]
296 def __setitem__(self, key: str, value: Any) -> None:
297 self._state[key] = value
299 def __delitem__(self, key: str) -> None:
300 del self._state[key]
302 def __len__(self) -> int:
303 return len(self._state)
305 def __iter__(self) -> Iterator[str]:
306 return iter(self._state)
308 ########
310 @reify
311 def secure(self) -> bool:
312 """A bool indicating if the request is handled with SSL."""
313 return self.scheme == "https"
315 @reify
316 def forwarded(self) -> Tuple[Mapping[str, str], ...]:
317 """A tuple containing all parsed Forwarded header(s).
319 Makes an effort to parse Forwarded headers as specified by RFC 7239:
321 - It adds one (immutable) dictionary per Forwarded 'field-value', ie
322 per proxy. The element corresponds to the data in the Forwarded
323 field-value added by the first proxy encountered by the client. Each
324 subsequent item corresponds to those added by later proxies.
325 - It checks that every value has valid syntax in general as specified
326 in section 4: either a 'token' or a 'quoted-string'.
327 - It un-escapes found escape sequences.
328 - It does NOT validate 'by' and 'for' contents as specified in section
329 6.
330 - It does NOT validate 'host' contents (Host ABNF).
331 - It does NOT validate 'proto' contents for valid URI scheme names.
333 Returns a tuple containing one or more immutable dicts
334 """
335 elems = []
336 for field_value in self._message.headers.getall(hdrs.FORWARDED, ()):
337 length = len(field_value)
338 pos = 0
339 need_separator = False
340 elem: Dict[str, str] = {}
341 elems.append(types.MappingProxyType(elem))
342 while 0 <= pos < length:
343 match = _FORWARDED_PAIR_RE.match(field_value, pos)
344 if match is not None: # got a valid forwarded-pair
345 if need_separator:
346 # bad syntax here, skip to next comma
347 pos = field_value.find(",", pos)
348 else:
349 name, value, port = match.groups()
350 if value[0] == '"':
351 # quoted string: remove quotes and unescape
352 value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1])
353 if port:
354 value += port
355 elem[name.lower()] = value
356 pos += len(match.group(0))
357 need_separator = True
358 elif field_value[pos] == ",": # next forwarded-element
359 need_separator = False
360 elem = {}
361 elems.append(types.MappingProxyType(elem))
362 pos += 1
363 elif field_value[pos] == ";": # next forwarded-pair
364 need_separator = False
365 pos += 1
366 elif field_value[pos] in " \t":
367 # Allow whitespace even between forwarded-pairs, though
368 # RFC 7239 doesn't. This simplifies code and is in line
369 # with Postel's law.
370 pos += 1
371 else:
372 # bad syntax here, skip to next comma
373 pos = field_value.find(",", pos)
374 return tuple(elems)
376 @reify
377 def scheme(self) -> str:
378 """A string representing the scheme of the request.
380 Hostname is resolved in this order:
382 - overridden value by .clone(scheme=new_scheme) call.
383 - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise.
385 'http' or 'https'.
386 """
387 if self._transport_sslcontext:
388 return "https"
389 else:
390 return "http"
392 @reify
393 def method(self) -> str:
394 """Read only property for getting HTTP method.
396 The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
397 """
398 return self._method
400 @reify
401 def version(self) -> HttpVersion:
402 """Read only property for getting HTTP version of request.
404 Returns aiohttp.protocol.HttpVersion instance.
405 """
406 return self._version
408 @reify
409 def host(self) -> str:
410 """Hostname of the request.
412 Hostname is resolved in this order:
414 - overridden value by .clone(host=new_host) call.
415 - HOST HTTP header
416 - socket.getfqdn() value
417 """
418 host = self._message.headers.get(hdrs.HOST)
419 if host is not None:
420 return host
421 return socket.getfqdn()
423 @reify
424 def remote(self) -> Optional[str]:
425 """Remote IP of client initiated HTTP request.
427 The IP is resolved in this order:
429 - overridden value by .clone(remote=new_remote) call.
430 - peername of opened socket
431 """
432 if self._transport_peername is None:
433 return None
434 if isinstance(self._transport_peername, (list, tuple)):
435 return str(self._transport_peername[0])
436 return str(self._transport_peername)
438 @reify
439 def url(self) -> URL:
440 url = URL.build(scheme=self.scheme, host=self.host)
441 return url.join(self._rel_url)
443 @reify
444 def path(self) -> str:
445 """The URL including *PATH INFO* without the host or scheme.
447 E.g., ``/app/blog``
448 """
449 return self._rel_url.path
451 @reify
452 def path_qs(self) -> str:
453 """The URL including PATH_INFO and the query string.
455 E.g, /app/blog?id=10
456 """
457 return str(self._rel_url)
459 @reify
460 def raw_path(self) -> str:
461 """The URL including raw *PATH INFO* without the host or scheme.
463 Warning, the path is unquoted and may contains non valid URL characters
465 E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters``
466 """
467 return self._message.path
469 @reify
470 def query(self) -> MultiDictProxy[str]:
471 """A multidict with all the variables in the query string."""
472 return MultiDictProxy(self._rel_url.query)
474 @reify
475 def query_string(self) -> str:
476 """The query string in the URL.
478 E.g., id=10
479 """
480 return self._rel_url.query_string
482 @reify
483 def headers(self) -> "CIMultiDictProxy[str]":
484 """A case-insensitive multidict proxy with all headers."""
485 return self._headers
487 @reify
488 def raw_headers(self) -> RawHeaders:
489 """A sequence of pairs for all headers."""
490 return self._message.raw_headers
492 @reify
493 def if_modified_since(self) -> Optional[datetime.datetime]:
494 """The value of If-Modified-Since HTTP header, or None.
496 This header is represented as a `datetime` object.
497 """
498 return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE))
500 @reify
501 def if_unmodified_since(self) -> Optional[datetime.datetime]:
502 """The value of If-Unmodified-Since HTTP header, or None.
504 This header is represented as a `datetime` object.
505 """
506 return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE))
508 @staticmethod
509 def _etag_values(etag_header: str) -> Iterator[ETag]:
510 """Extract `ETag` objects from raw header."""
511 if etag_header == ETAG_ANY:
512 yield ETag(
513 is_weak=False,
514 value=ETAG_ANY,
515 )
516 else:
517 for match in LIST_QUOTED_ETAG_RE.finditer(etag_header):
518 is_weak, value, garbage = match.group(2, 3, 4)
519 # Any symbol captured by 4th group means
520 # that the following sequence is invalid.
521 if garbage:
522 break
524 yield ETag(
525 is_weak=bool(is_weak),
526 value=value,
527 )
529 @classmethod
530 def _if_match_or_none_impl(
531 cls, header_value: Optional[str]
532 ) -> Optional[Tuple[ETag, ...]]:
533 if not header_value:
534 return None
536 return tuple(cls._etag_values(header_value))
538 @reify
539 def if_match(self) -> Optional[Tuple[ETag, ...]]:
540 """The value of If-Match HTTP header, or None.
542 This header is represented as a `tuple` of `ETag` objects.
543 """
544 return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH))
546 @reify
547 def if_none_match(self) -> Optional[Tuple[ETag, ...]]:
548 """The value of If-None-Match HTTP header, or None.
550 This header is represented as a `tuple` of `ETag` objects.
551 """
552 return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH))
554 @reify
555 def if_range(self) -> Optional[datetime.datetime]:
556 """The value of If-Range HTTP header, or None.
558 This header is represented as a `datetime` object.
559 """
560 return parse_http_date(self.headers.get(hdrs.IF_RANGE))
562 @reify
563 def keep_alive(self) -> bool:
564 """Is keepalive enabled by client?"""
565 return not self._message.should_close
567 @reify
568 def cookies(self) -> Mapping[str, str]:
569 """Return request cookies.
571 A read-only dictionary-like object.
572 """
573 raw = self.headers.get(hdrs.COOKIE, "")
574 parsed: SimpleCookie[str] = SimpleCookie(raw)
575 return MappingProxyType({key: val.value for key, val in parsed.items()})
577 @reify
578 def http_range(self) -> slice:
579 """The content of Range HTTP header.
581 Return a slice instance.
583 """
584 rng = self._headers.get(hdrs.RANGE)
585 start, end = None, None
586 if rng is not None:
587 try:
588 pattern = r"^bytes=(\d*)-(\d*)$"
589 start, end = re.findall(pattern, rng)[0]
590 except IndexError: # pattern was not found in header
591 raise ValueError("range not in acceptable format")
593 end = int(end) if end else None
594 start = int(start) if start else None
596 if start is None and end is not None:
597 # end with no start is to return tail of content
598 start = -end
599 end = None
601 if start is not None and end is not None:
602 # end is inclusive in range header, exclusive for slice
603 end += 1
605 if start >= end:
606 raise ValueError("start cannot be after end")
608 if start is end is None: # No valid range supplied
609 raise ValueError("No start or end of range specified")
611 return slice(start, end, 1)
613 @reify
614 def content(self) -> StreamReader:
615 """Return raw payload stream."""
616 return self._payload
618 @property
619 def can_read_body(self) -> bool:
620 """Return True if request's HTTP BODY can be read, False otherwise."""
621 return not self._payload.at_eof()
623 @reify
624 def body_exists(self) -> bool:
625 """Return True if request has HTTP BODY, False otherwise."""
626 return type(self._payload) is not EmptyStreamReader
628 async def release(self) -> None:
629 """Release request.
631 Eat unread part of HTTP BODY if present.
632 """
633 while not self._payload.at_eof():
634 await self._payload.readany()
636 async def read(self) -> bytes:
637 """Read request body if present.
639 Returns bytes object with full request content.
640 """
641 if self._read_bytes is None:
642 body = bytearray()
643 while True:
644 chunk = await self._payload.readany()
645 body.extend(chunk)
646 if self._client_max_size:
647 body_size = len(body)
648 if body_size > self._client_max_size:
649 raise HTTPRequestEntityTooLarge(
650 max_size=self._client_max_size, actual_size=body_size
651 )
652 if not chunk:
653 break
654 self._read_bytes = bytes(body)
655 return self._read_bytes
657 async def text(self) -> str:
658 """Return BODY as text using encoding from .charset."""
659 bytes_body = await self.read()
660 encoding = self.charset or "utf-8"
661 try:
662 return bytes_body.decode(encoding)
663 except LookupError:
664 raise HTTPUnsupportedMediaType()
666 async def json(
667 self,
668 *,
669 loads: JSONDecoder = DEFAULT_JSON_DECODER,
670 content_type: Optional[str] = "application/json",
671 ) -> Any:
672 """Return BODY as JSON."""
673 body = await self.text()
674 if content_type:
675 if not is_expected_content_type(self.content_type, content_type):
676 raise HTTPBadRequest(
677 text=(
678 "Attempt to decode JSON with "
679 "unexpected mimetype: %s" % self.content_type
680 )
681 )
683 return loads(body)
685 async def multipart(self) -> MultipartReader:
686 """Return async iterator to process BODY as multipart."""
687 return MultipartReader(self._headers, self._payload)
689 async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]":
690 """Return POST parameters."""
691 if self._post is not None:
692 return self._post
693 if self._method not in self.POST_METHODS:
694 self._post = MultiDictProxy(MultiDict())
695 return self._post
697 content_type = self.content_type
698 if content_type not in (
699 "",
700 "application/x-www-form-urlencoded",
701 "multipart/form-data",
702 ):
703 self._post = MultiDictProxy(MultiDict())
704 return self._post
706 out: MultiDict[Union[str, bytes, FileField]] = MultiDict()
708 if content_type == "multipart/form-data":
709 multipart = await self.multipart()
710 max_size = self._client_max_size
712 field = await multipart.next()
713 while field is not None:
714 size = 0
715 field_ct = field.headers.get(hdrs.CONTENT_TYPE)
717 if isinstance(field, BodyPartReader):
718 assert field.name is not None
720 # Note that according to RFC 7578, the Content-Type header
721 # is optional, even for files, so we can't assume it's
722 # present.
723 # https://tools.ietf.org/html/rfc7578#section-4.4
724 if field.filename:
725 # store file in temp file
726 tmp = tempfile.TemporaryFile()
727 chunk = await field.read_chunk(size=2**16)
728 while chunk:
729 chunk = field.decode(chunk)
730 tmp.write(chunk)
731 size += len(chunk)
732 if 0 < max_size < size:
733 tmp.close()
734 raise HTTPRequestEntityTooLarge(
735 max_size=max_size, actual_size=size
736 )
737 chunk = await field.read_chunk(size=2**16)
738 tmp.seek(0)
740 if field_ct is None:
741 field_ct = "application/octet-stream"
743 ff = FileField(
744 field.name,
745 field.filename,
746 cast(io.BufferedReader, tmp),
747 field_ct,
748 field.headers,
749 )
750 out.add(field.name, ff)
751 else:
752 # deal with ordinary data
753 value = await field.read(decode=True)
754 if field_ct is None or field_ct.startswith("text/"):
755 charset = field.get_charset(default="utf-8")
756 out.add(field.name, value.decode(charset))
757 else:
758 out.add(field.name, value)
759 size += len(value)
760 if 0 < max_size < size:
761 raise HTTPRequestEntityTooLarge(
762 max_size=max_size, actual_size=size
763 )
764 else:
765 raise ValueError(
766 "To decode nested multipart you need " "to use custom reader",
767 )
769 field = await multipart.next()
770 else:
771 data = await self.read()
772 if data:
773 charset = self.charset or "utf-8"
774 bytes_query = data.rstrip()
775 try:
776 query = bytes_query.decode(charset)
777 except LookupError:
778 raise HTTPUnsupportedMediaType()
779 out.extend(
780 parse_qsl(qs=query, keep_blank_values=True, encoding=charset)
781 )
783 self._post = MultiDictProxy(out)
784 return self._post
786 def get_extra_info(self, name: str, default: Any = None) -> Any:
787 """Extra info from protocol transport"""
788 protocol = self._protocol
789 if protocol is None:
790 return default
792 transport = protocol.transport
793 if transport is None:
794 return default
796 return transport.get_extra_info(name, default)
798 def __repr__(self) -> str:
799 ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode(
800 "ascii"
801 )
802 return "<{} {} {} >".format(
803 self.__class__.__name__, self._method, ascii_encodable_path
804 )
806 def __eq__(self, other: object) -> bool:
807 return id(self) == id(other)
809 def __bool__(self) -> bool:
810 return True
812 async def _prepare_hook(self, response: StreamResponse) -> None:
813 return
815 def _cancel(self, exc: BaseException) -> None:
816 self._payload.set_exception(exc)
817 for fut in self._disconnection_waiters:
818 set_result(fut, None)
820 def _finish(self) -> None:
821 for fut in self._disconnection_waiters:
822 fut.cancel()
824 if self._post is None or self.content_type != "multipart/form-data":
825 return
827 # NOTE: Release file descriptors for the
828 # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom`
829 # NOTE: instances of files sent within multipart request body
830 # NOTE: via HTTP POST request.
831 for file_name, file_field_object in self._post.items():
832 if not isinstance(file_field_object, FileField):
833 continue
835 file_field_object.file.close()
837 async def wait_for_disconnection(self) -> None:
838 loop = asyncio.get_event_loop()
839 fut: asyncio.Future[None] = loop.create_future()
840 self._disconnection_waiters.add(fut)
841 try:
842 await fut
843 finally:
844 self._disconnection_waiters.remove(fut)
847class Request(BaseRequest):
848 __slots__ = ("_match_info",)
850 def __init__(self, *args: Any, **kwargs: Any) -> None:
851 super().__init__(*args, **kwargs)
853 # matchdict, route_name, handler
854 # or information about traversal lookup
856 # initialized after route resolving
857 self._match_info: Optional[UrlMappingMatchInfo] = None
859 def clone(
860 self,
861 *,
862 method: Union[str, _SENTINEL] = sentinel,
863 rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
864 headers: Union[LooseHeaders, _SENTINEL] = sentinel,
865 scheme: Union[str, _SENTINEL] = sentinel,
866 host: Union[str, _SENTINEL] = sentinel,
867 remote: Union[str, _SENTINEL] = sentinel,
868 client_max_size: Union[int, _SENTINEL] = sentinel,
869 ) -> "Request":
870 ret = super().clone(
871 method=method,
872 rel_url=rel_url,
873 headers=headers,
874 scheme=scheme,
875 host=host,
876 remote=remote,
877 client_max_size=client_max_size,
878 )
879 new_ret = cast(Request, ret)
880 new_ret._match_info = self._match_info
881 return new_ret
883 @reify
884 def match_info(self) -> "UrlMappingMatchInfo":
885 """Result of route resolving."""
886 match_info = self._match_info
887 assert match_info is not None
888 return match_info
890 @property
891 def app(self) -> "Application":
892 """Application instance."""
893 match_info = self._match_info
894 assert match_info is not None
895 return match_info.current_app
897 @property
898 def config_dict(self) -> ChainMapProxy:
899 match_info = self._match_info
900 assert match_info is not None
901 lst = match_info.apps
902 app = self.app
903 idx = lst.index(app)
904 sublist = list(reversed(lst[: idx + 1]))
905 return ChainMapProxy(sublist)
907 async def _prepare_hook(self, response: StreamResponse) -> None:
908 match_info = self._match_info
909 if match_info is None:
910 return
911 for app in match_info._apps:
912 await app.on_response_prepare.send(self, response)