88from collections .abc import Iterator , MutableMapping
99from concurrent .futures import Executor
1010from http import HTTPStatus
11- from typing import TYPE_CHECKING , Any , Optional , Union , cast
11+ from typing import TYPE_CHECKING , Any , Optional , TypeVar , Union , cast , overload
1212
1313from multidict import CIMultiDict , istr
1414
2121 CookieMixin ,
2222 ETag ,
2323 HeadersMixin ,
24+ ResponseKey ,
2425 must_be_empty_body ,
2526 parse_http_date ,
2627 populate_with_cookies ,
3233from .http import SERVER_SOFTWARE , HttpVersion10 , HttpVersion11
3334from .payload import Payload
3435from .typedefs import JSONEncoder , LooseHeaders
36+ from .web_exceptions import NotAppKeyWarning
3537
3638REASON_PHRASES = {http_status .value : http_status .phrase for http_status in HTTPStatus }
3739LARGE_BODY_SIZE = 1024 ** 2
4345 from .web_request import BaseRequest
4446
4547
48+ _T = TypeVar ("_T" )
49+
50+
4651# TODO(py311): Convert to StrEnum for wider use
4752class ContentCoding (enum .Enum ):
4853 # The content codings that we have support for.
@@ -61,7 +66,9 @@ class ContentCoding(enum.Enum):
6166############################################################
6267
6368
64- class StreamResponse (MutableMapping [str , Any ], HeadersMixin , CookieMixin ):
69+ class StreamResponse (
70+ MutableMapping [str | ResponseKey [Any ], Any ], HeadersMixin , CookieMixin
71+ ):
6572
6673 _body : None | bytes | bytearray | Payload
6774 _length_check = True
@@ -77,6 +84,7 @@ class StreamResponse(MutableMapping[str, Any], HeadersMixin, CookieMixin):
7784 _must_be_empty_body : bool | None = None
7885 _body_length = 0
7986 _send_headers_immediately = True
87+ _seen_str_keys : set [str ] = set ()
8088
8189 def __init__ (
8290 self ,
@@ -93,7 +101,7 @@ def __init__(
93101 the headers when creating a new response object. It is not intended
94102 to be used by external code.
95103 """
96- self ._state : dict [str , Any ] = {}
104+ self ._state : dict [str | ResponseKey [ Any ] , Any ] = {}
97105
98106 if _real_headers is not None :
99107 self ._headers = _real_headers
@@ -483,19 +491,43 @@ def __repr__(self) -> str:
483491 info = "not prepared"
484492 return f"<{ self .__class__ .__name__ } { self .reason } { info } >"
485493
486- def __getitem__ (self , key : str ) -> Any :
494+ @overload # type: ignore[override]
495+ def __getitem__ (self , key : ResponseKey [_T ]) -> _T : ...
496+
497+ @overload
498+ def __getitem__ (self , key : str ) -> Any : ...
499+
500+ def __getitem__ (self , key : str | ResponseKey [_T ]) -> Any :
487501 return self ._state [key ]
488502
489- def __setitem__ (self , key : str , value : Any ) -> None :
503+ @overload # type: ignore[override]
504+ def __setitem__ (self , key : ResponseKey [_T ], value : _T ) -> None : ...
505+
506+ @overload
507+ def __setitem__ (self , key : str , value : Any ) -> None : ...
508+
509+ def __setitem__ (self , key : str | ResponseKey [_T ], value : Any ) -> None :
510+ if (
511+ not isinstance (key , ResponseKey )
512+ and key not in StreamResponse ._seen_str_keys
513+ ):
514+ StreamResponse ._seen_str_keys .add (key )
515+ warnings .warn (
516+ "It is recommended to use web.ResponseKey instances for keys.\n "
517+ + "https://docs.aiohttp.org/en/stable/web_advanced.html"
518+ + "#response-s-storage" ,
519+ category = NotAppKeyWarning ,
520+ stacklevel = 2 ,
521+ )
490522 self ._state [key ] = value
491523
492- def __delitem__ (self , key : str ) -> None :
524+ def __delitem__ (self , key : str | ResponseKey [ _T ] ) -> None :
493525 del self ._state [key ]
494526
495527 def __len__ (self ) -> int :
496528 return len (self ._state )
497529
498- def __iter__ (self ) -> Iterator [str ]:
530+ def __iter__ (self ) -> Iterator [str | ResponseKey [ Any ] ]:
499531 return iter (self ._state )
500532
501533 def __hash__ (self ) -> int :
0 commit comments