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