diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9a366681b87..ef40c7aa512 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -190,30 +190,22 @@ jobs: submodules: true - name: Setup Python ${{ matrix.pyver }} id: python-install - uses: actions/setup-python@v6 + # important: do not use system python + env: + UV_PYTHON_PREFERENCE: only-managed + uses: astral-sh/setup-uv@v8.1.0 with: - allow-prereleases: true python-version: ${{ matrix.pyver }} - - name: Get pip cache dir - id: pip-cache - run: | - echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}" - shell: bash - - name: Cache PyPI - uses: actions/cache@v5.0.5 - with: - key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }} - path: ${{ steps.pip-cache.outputs.dir }} - restore-keys: | - pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}- + activate-environment: true + enable-cache: true - name: Update pip, wheel, setuptools, build, twine run: | - python -m pip install -U pip wheel setuptools build twine + uv pip install -U pip wheel setuptools build twine - name: Install dependencies env: DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }} run: | - python -Im pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt + uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt - name: Set PYTHON_GIL=0 for free-threading builds if: ${{ endsWith(matrix.pyver, 't') }} run: echo "PYTHON_GIL=0" >> $GITHUB_ENV @@ -230,7 +222,7 @@ jobs: - name: Install self env: AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }} - run: python -m pip install -e . + run: uv pip install -e . - name: Run unittests env: COLOR: yes @@ -303,30 +295,22 @@ jobs: submodules: true - name: Setup Python ${{ matrix.pyver }} id: python-install - uses: actions/setup-python@v6 + # important: do not use system python + env: + UV_PYTHON_PREFERENCE: only-managed + uses: astral-sh/setup-uv@v8.1.0 with: - allow-prereleases: true python-version: ${{ matrix.pyver }} - - name: Get pip cache dir - id: pip-cache - run: | - echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}" - shell: bash - - name: Cache PyPI - uses: actions/cache@v5.0.5 - with: - key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }} - path: ${{ steps.pip-cache.outputs.dir }} - restore-keys: | - pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}- + activate-environment: true + enable-cache: true - name: Update pip, wheel, setuptools, build, twine run: | - python -m pip install -U pip wheel setuptools build twine + uv pip install -U pip wheel setuptools build twine - name: Install dependencies env: DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }} run: | - python -Im pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt + uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt - name: Restore llhttp generated files if: ${{ matrix.no-extensions == '' }} uses: actions/download-artifact@v8 @@ -340,7 +324,7 @@ jobs: - name: Install self env: AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }} - run: python -m pip install -e . + run: uv pip install -e . - name: Run unittests env: COLOR: yes diff --git a/CHANGES.rst b/CHANGES.rst index 2e30443b567..6082065d8de 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5674,7 +5674,7 @@ Bugfixes `_) - Add ``app.pre_frozen`` state to properly handle startup signals in sub-applications. (`#3237 `_) -- Enhanced parsing and validation of helpers.BasicAuth.decode. (`#3239 +- Enhanced parsing and validation of ``helpers.BasicAuth.decode``. (`#3239 `_) - Change imports from collections module in preparation for 3.8. (`#3258 `_) @@ -7155,7 +7155,7 @@ Misc * Drop old-style routes: `Route`, `PlainRoute`, `DynamicRoute`, `StaticRoute`, `ResourceAdapter`. - Revert `resp.url` back to `str`, introduce `resp.url_obj` (`#1292 `_) -- Raise ValueError if BasicAuth login has a ":" character (`#1307 `_) +- Raise ValueError if ``BasicAuth`` login has a ":" character (`#1307 `_) - Fix bug when ClientRequest send payload file with opened as open('filename', 'r+b') (`#1306 `_) - Enhancement to AccessLogger (pass *extra* dict) (`#1303 `_) @@ -7403,7 +7403,7 @@ Misc `aiohttp.worker.GunicornUVLoopWebWorker` (`#878 `_) - Don't send body in response to HEAD request (`#838 `_) - Skip the preamble in MultipartReader (`#881 `_) -- Implement BasicAuth decode classmethod. (`#744 `_) +- Implement ``BasicAuth`` decode classmethod. (`#744 `_) - Don't crash logger when transport is None (`#889 `_) - Use a create_future compatibility wrapper instead of creating Futures directly (`#896 `_) @@ -7429,7 +7429,7 @@ Misc - Separate sending file logic from StaticRoute dispatcher (`#901 `_) - Drop deprecated share_cookies connector option (BACKWARD INCOMPATIBLE) - Drop deprecated support for tuple as auth parameter. - Use aiohttp.BasicAuth instead (BACKWARD INCOMPATIBLE) + Use ``aiohttp.BasicAuth`` instead (BACKWARD INCOMPATIBLE) - Remove deprecated `request.payload` property, use `content` instead. (BACKWARD INCOMPATIBLE) - Drop all mentions about api changes in documentation for versions diff --git a/CHANGES/12499.deprecation.rst b/CHANGES/12499.deprecation.rst index c9a5b533c77..7c437b4e998 100644 --- a/CHANGES/12499.deprecation.rst +++ b/CHANGES/12499.deprecation.rst @@ -1,4 +1,4 @@ -Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth`` +Deprecated ``BasicAuth`` and the ``auth`` / ``proxy_auth`` parameters. They will be removed in aiohttp 4.0. Use the new :func:`~aiohttp.encode_basic_auth` helper together with ``headers={"Authorization": ...}`` (or diff --git a/CHANGES/12499.feature.rst b/CHANGES/12499.feature.rst index 8b242432367..0350649e07d 100644 --- a/CHANGES/12499.feature.rst +++ b/CHANGES/12499.feature.rst @@ -1,3 +1,3 @@ Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic Authentication credentials. Replaces the now-deprecated -:class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`. +``BasicAuth`` -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12501.breaking.rst b/CHANGES/12501.breaking.rst new file mode 100644 index 00000000000..8687ea8558e --- /dev/null +++ b/CHANGES/12501.breaking.rst @@ -0,0 +1,3 @@ +Removed ``BasicAuth`` and the ``auth`` / ``proxy_auth`` parameters. +Use ``encode_basic_auth()`` in a header instead. +-- by :user:`Dreamsorcerer` diff --git a/CHANGES/12629.contrib.rst b/CHANGES/12629.contrib.rst new file mode 100644 index 00000000000..8c10b198425 --- /dev/null +++ b/CHANGES/12629.contrib.rst @@ -0,0 +1,7 @@ +Switched the CI ``test`` and ``autobahn`` jobs from +``actions/setup-python`` to ``astral-sh/setup-uv`` for installing +interpreters, cutting the ``Setup Python`` step from 40-58s to a +few seconds on ``macos-latest`` and ``windows-latest`` runners for +variants not in the hosted tool-cache (notably the free-threaded +``3.14t``) +-- by :user:`bdraco`. diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 59fc64160f1..64bc3ced0a1 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -53,7 +53,7 @@ from .connector import AddrInfoType, SocketFactoryType from .cookiejar import CookieJar, DummyCookieJar from .formdata import FormData -from .helpers import BasicAuth, ChainMapProxy, ETag, encode_basic_auth +from .helpers import ChainMapProxy, ETag, encode_basic_auth from .http import ( HttpVersion, HttpVersion10, @@ -168,7 +168,6 @@ # formdata "FormData", # helpers - "BasicAuth", "ChainMapProxy", "DigestAuthMiddleware", "ETag", diff --git a/aiohttp/client.py b/aiohttp/client.py index eb2d5e880d5..7c2535fc8ee 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -94,9 +94,8 @@ _SENTINEL, DEFAULT_CHUNK_SIZE, EMPTY_BODY_METHODS, - BasicAuth, TimeoutHandle, - basicauth_from_netrc, + _auth_header_from_netrc, frozen_dataclass_decorator, get_env_proxy_for_url, netrc_from_env, @@ -181,7 +180,6 @@ class _RequestOptions(TypedDict, total=False): cookies: LooseCookies | None headers: LooseHeaders | None skip_auto_headers: Iterable[str] | None - auth: BasicAuth | None allow_redirects: bool max_redirects: int compress: Literal["deflate", "gzip"] | bool @@ -190,7 +188,6 @@ class _RequestOptions(TypedDict, total=False): raise_for_status: None | bool | Callable[[ClientResponse], Awaitable[None]] read_until_eof: bool proxy: StrOrURL | None - proxy_auth: BasicAuth | None timeout: "ClientTimeout | _SENTINEL | None" ssl: SSLContext | bool | Fingerprint server_hostname: str | None @@ -212,12 +209,10 @@ class _WSConnectOptions(TypedDict, total=False): autoclose: bool autoping: bool heartbeat: float | None - auth: BasicAuth | None origin: str | None params: Query headers: LooseHeaders | None proxy: StrOrURL | None - proxy_auth: BasicAuth | None ssl: SSLContext | bool | Fingerprint server_hostname: str | None proxy_headers: LooseHeaders | None @@ -281,7 +276,6 @@ class ClientSession: "_loop", "_cookie_jar", "_connector_owner", - "_default_auth", "_version", "_json_serialize", "_json_serialize_bytes", @@ -302,7 +296,6 @@ class ClientSession: "_max_headers", "_resolve_charset", "_default_proxy", - "_default_proxy_auth", "_retry_connection", "_middlewares", ) @@ -315,9 +308,7 @@ def __init__( cookies: LooseCookies | None = None, headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, - proxy_auth: BasicAuth | None = None, skip_auto_headers: Iterable[str] | None = None, - auth: BasicAuth | None = None, json_serialize: JSONEncoder = json.dumps, json_serialize_bytes: JSONBytesEncoder | None = None, request_class: type[ClientRequest] = ClientRequest, @@ -394,24 +385,7 @@ def __init__( if cookies: self._cookie_jar.update_cookies(cookies) - if auth is not None: - warnings.warn( - "The 'auth' parameter is deprecated and will be removed in v4;" - " pass headers={'Authorization': " - "aiohttp.encode_basic_auth(login, password)} instead", - DeprecationWarning, - stacklevel=2, - ) - if proxy_auth is not None: - warnings.warn( - "The 'proxy_auth' parameter is deprecated and will be removed in v4;" - " pass proxy_headers={'Proxy-Authorization': " - "aiohttp.encode_basic_auth(login, password)} instead", - DeprecationWarning, - stacklevel=2, - ) self._connector_owner = connector_owner - self._default_auth = auth self._version = version self._json_serialize = json_serialize self._json_serialize_bytes = json_serialize_bytes @@ -446,7 +420,6 @@ def __init__( self._resolve_charset = fallback_charset_resolver self._default_proxy = proxy - self._default_proxy_auth = proxy_auth self._retry_connection: bool = True self._middlewares = middlewares @@ -501,7 +474,6 @@ async def _request( cookies: LooseCookies | None = None, headers: LooseHeaders | None = None, skip_auto_headers: Iterable[str] | None = None, - auth: BasicAuth | None = None, allow_redirects: bool = True, max_redirects: int = 10, compress: Literal["deflate", "gzip"] | bool = False, @@ -512,7 +484,6 @@ async def _request( ) = None, read_until_eof: bool = True, proxy: StrOrURL | None = None, - proxy_auth: BasicAuth | None = None, timeout: ClientTimeout | _SENTINEL | None = sentinel, ssl: SSLContext | bool | Fingerprint = True, server_hostname: str | None = None, @@ -532,23 +503,6 @@ async def _request( if self.closed: raise RuntimeError("Session is closed") - if auth is not None: - warnings.warn( - "The 'auth' parameter is deprecated and will be removed in v4;" - " pass headers={'Authorization': " - "aiohttp.encode_basic_auth(login, password)} instead", - DeprecationWarning, - stacklevel=3, - ) - if proxy_auth is not None: - warnings.warn( - "The 'proxy_auth' parameter is deprecated and will be removed in v4;" - " pass proxy_headers={'Proxy-Authorization': " - "aiohttp.encode_basic_auth(login, password)} instead", - DeprecationWarning, - stacklevel=3, - ) - if not isinstance(ssl, SSL_ALLOWED_TYPES): raise TypeError( "ssl should be SSLContext, Fingerprint, or bool, " @@ -594,8 +548,6 @@ async def _request( if proxy is None: proxy = self._default_proxy - if proxy_auth is None: - proxy_auth = self._default_proxy_auth if proxy is None: proxy_headers = None @@ -662,43 +614,29 @@ async def _request( else InvalidUrlClientError ) raise err_exc_cls(url) - # If `auth` was passed for an already authenticated URL, - # disallow only if this is the initial URL; this is to avoid issues - # with sketchy redirects that are not the caller's responsibility - if not history and (auth and auth_from_url): - raise ValueError( - "Cannot combine AUTH argument with " - "credentials encoded in URL" - ) - # Override the auth with the one from the URL only if we - # have no auth, or if we got an auth from a redirect URL - if auth is None or (history and auth_from_url is not None): - auth = auth_from_url - - if ( - auth is None - and self._default_auth - and ( - not self._base_url or self._base_url_origin == url.origin() - ) + if auth_from_url is not None: + # URL-embedded credentials override any Authorization + # header already present (e.g. carried from a previous + # redirect). On the initial request, refuse to silently + # shadow an explicit Authorization header. + if not history and hdrs.AUTHORIZATION in headers: + raise ValueError( + "Cannot combine AUTHORIZATION header with " + "credentials encoded in URL" + ) + headers[hdrs.AUTHORIZATION] = auth_from_url + elif ( + self._trust_env + and url.host is not None + and hdrs.AUTHORIZATION not in headers ): - auth = self._default_auth - - # Try netrc if auth is still None and trust_env is enabled. - if auth is None and self._trust_env and url.host is not None: - auth = await self._loop.run_in_executor( + # Fall back to ~/.netrc credentials when trust_env is set. + netrc_auth = await self._loop.run_in_executor( None, self._get_netrc_auth, url.host ) - - # It would be confusing if we support explicit - # Authorization header with auth argument - if auth is not None and hdrs.AUTHORIZATION in headers: - raise ValueError( - "Cannot combine AUTHORIZATION header " - "with AUTH argument or credentials " - "encoded in URL" - ) + if netrc_auth is not None: + headers[hdrs.AUTHORIZATION] = netrc_auth all_cookies = self._cookie_jar.filter_cookies(url) @@ -717,9 +655,15 @@ async def _request( proxy_ = URL(proxy) elif self._trust_env: with suppress(LookupError): - proxy_, proxy_auth = await asyncio.to_thread( + proxy_, env_proxy_auth = await asyncio.to_thread( get_env_proxy_for_url, url ) + if env_proxy_auth is not None and ( + proxy_headers is None + or hdrs.PROXY_AUTHORIZATION not in proxy_headers + ): + proxy_headers = proxy_headers or CIMultiDict() + proxy_headers[hdrs.PROXY_AUTHORIZATION] = env_proxy_auth req = self._request_class( method, @@ -729,7 +673,6 @@ async def _request( skip_auto_headers=skip_headers, data=data, cookies=all_cookies, - auth=auth, version=version, compress=compress, chunked=chunked, @@ -737,7 +680,6 @@ async def _request( loop=self._loop, response_class=self._response_class, proxy=proxy_, - proxy_auth=proxy_auth, timer=timer, session=self, ssl=ssl, @@ -915,7 +857,6 @@ async def _connect_and_send_request( ) from origin_val_err if url.origin() != redirect_origin: - auth = None headers.pop(hdrs.AUTHORIZATION, None) headers.pop(hdrs.COOKIE, None) headers.pop(hdrs.PROXY_AUTHORIZATION, None) @@ -1006,12 +947,10 @@ def ws_connect( autoclose: bool = True, autoping: bool = True, heartbeat: float | None = None, - auth: BasicAuth | None = None, origin: str | None = None, params: Query = None, headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, - proxy_auth: BasicAuth | None = None, ssl: SSLContext | bool | Fingerprint = True, server_hostname: str | None = None, proxy_headers: LooseHeaders | None = None, @@ -1030,12 +969,10 @@ def ws_connect( autoclose=autoclose, autoping=autoping, heartbeat=heartbeat, - auth=auth, origin=origin, params=params, headers=headers, proxy=proxy, - proxy_auth=proxy_auth, ssl=ssl, server_hostname=server_hostname, proxy_headers=proxy_headers, @@ -1085,12 +1022,10 @@ async def _ws_connect( autoclose: bool = True, autoping: bool = True, heartbeat: float | None = None, - auth: BasicAuth | None = None, origin: str | None = None, params: Query = None, headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, - proxy_auth: BasicAuth | None = None, ssl: SSLContext | bool | Fingerprint = True, server_hostname: str | None = None, proxy_headers: LooseHeaders | None = None, @@ -1098,22 +1033,6 @@ async def _ws_connect( max_msg_size: int = 4 * 1024 * 1024, decode_text: bool = True, ) -> "ClientWebSocketResponse[bool]": - if auth is not None: - warnings.warn( - "The 'auth' parameter is deprecated and will be removed in v4;" - " pass headers={'Authorization': " - "aiohttp.encode_basic_auth(login, password)} instead", - DeprecationWarning, - stacklevel=3, - ) - if proxy_auth is not None: - warnings.warn( - "The 'proxy_auth' parameter is deprecated and will be removed in v4;" - " pass proxy_headers={'Proxy-Authorization': " - "aiohttp.encode_basic_auth(login, password)} instead", - DeprecationWarning, - stacklevel=3, - ) if timeout is not sentinel: if isinstance(timeout, ClientWSTimeout): ws_timeout = timeout @@ -1176,9 +1095,7 @@ async def _ws_connect( params=params, headers=real_headers, read_until_eof=False, - auth=auth, proxy=proxy, - proxy_auth=proxy_auth, ssl=ssl, server_hostname=server_hostname, proxy_headers=proxy_headers, @@ -1320,16 +1237,15 @@ def _prepare_headers(self, headers: LooseHeaders | None) -> "CIMultiDict[str]": added_names.add(key) return result - def _get_netrc_auth(self, host: str) -> BasicAuth | None: - """ - Get auth from netrc for the given host. + def _get_netrc_auth(self, host: str) -> str | None: + """Return an ``Authorization`` header value for ``host`` from netrc. - This method is designed to be called in an executor to avoid - blocking I/O in the event loop. + Designed to be called in an executor to avoid blocking I/O on the + event loop. """ netrc_obj = netrc_from_env() try: - return basicauth_from_netrc(netrc_obj, host) + return _auth_header_from_netrc(netrc_obj, host) except LookupError: return None @@ -1492,11 +1408,6 @@ def skip_auto_headers(self) -> frozenset[istr]: """Headers for which autogeneration should be skipped""" return self._skip_auto_headers - @property - def auth(self) -> BasicAuth | None: - """An object that represents HTTP Basic Authorization""" - return self._default_auth - @property def json_serialize(self) -> JSONEncoder: """Json serializer callable""" @@ -1660,8 +1571,6 @@ def request( headers - (optional) Dictionary of HTTP Headers to send with the request cookies - (optional) Dict object to send with the request - auth - (optional) BasicAuth named tuple represent HTTP Basic Auth - auth - aiohttp.helpers.BasicAuth allow_redirects - (optional) If set to False, do not follow redirects version - Request HTTP version. diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 8c353822b58..1b3a1197c07 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -37,11 +37,10 @@ from .helpers import ( _SENTINEL, BaseTimerContext, - BasicAuth, HeadersDictProxy, HeadersMixin, TimerNoop, - _basic_auth_no_warn, + encode_basic_auth, frozen_dataclass_decorator, is_expected_content_type, parse_mimetype, @@ -180,7 +179,6 @@ class ConnectionKey(NamedTuple): is_ssl: bool ssl: SSLContext | bool | Fingerprint proxy: URL | None - proxy_auth: BasicAuth | None proxy_headers_hash: int | None # hash(CIMultiDict) @@ -685,7 +683,6 @@ class ClientRequestBase: POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT} - auth = None proxy: URL | None = None response_class = ClientResponse server_hostname: str | None = None # Needed in connector.py @@ -712,7 +709,6 @@ def __init__( url: URL, *, headers: CIMultiDict[str], - auth: BasicAuth | None, loop: asyncio.AbstractEventLoop, ssl: SSLContext | bool | Fingerprint, trust_env: bool = False, @@ -733,9 +729,13 @@ def __init__( if loop.get_debug(): self._source_traceback = traceback.extract_stack(sys._getframe(1)) - self._update_host(url) + if not url.raw_host: + raise InvalidURL(url) self._update_headers(headers) - self._update_auth(auth, trust_env) + if url.raw_user or url.raw_password: + self.headers[hdrs.AUTHORIZATION] = encode_basic_auth( + url.user or "", url.password or "" + ) def _reset_writer(self, _: object = None) -> None: self._writer_task = None @@ -784,32 +784,9 @@ def connection_key(self) -> ConnectionKey: self._ssl, None, None, - None, ), ) - def _update_auth(self, auth: BasicAuth | None, trust_env: bool = False) -> None: - """Set basic auth.""" - if auth is None: - auth = self.auth - if auth is None: - return - - if not isinstance(auth, BasicAuth): - raise TypeError("BasicAuth() tuple is required instead") - - self.headers[hdrs.AUTHORIZATION] = auth.encode() - - def _update_host(self, url: URL) -> None: - """Update destination host, port and connection type (ssl).""" - # get host/port - if not url.raw_host: - raise InvalidURL(url) - - # basic auth info - if url.raw_user or url.raw_password: - self.auth = _basic_auth_no_warn(url.user or "", url.password or "") - def _update_headers(self, headers: CIMultiDict[str]) -> None: """Update request headers.""" self.headers: CIMultiDict[str] = CIMultiDict() @@ -927,7 +904,6 @@ class ClientRequestArgs(TypedDict, total=False): skip_auto_headers: Iterable[str] | None data: Any cookies: BaseCookie[str] - auth: BasicAuth | None version: HttpVersion compress: Literal["deflate", "gzip"] | bool chunked: bool | None @@ -935,7 +911,6 @@ class ClientRequestArgs(TypedDict, total=False): loop: asyncio.AbstractEventLoop response_class: type[ClientResponse] proxy: URL | None - proxy_auth: BasicAuth | None timer: BaseTimerContext session: "ClientSession" ssl: SSLContext | bool | Fingerprint @@ -971,7 +946,6 @@ def __init__( skip_auto_headers: Iterable[str] | None, data: Any, cookies: BaseCookie[str], - auth: BasicAuth | None, version: HttpVersion, compress: Literal["deflate", "gzip"] | bool, chunked: bool | None, @@ -979,7 +953,6 @@ def __init__( loop: asyncio.AbstractEventLoop, response_class: type[ClientResponse], proxy: URL | None, - proxy_auth: BasicAuth | None, timer: BaseTimerContext, session: "ClientSession", ssl: SSLContext | bool | Fingerprint, @@ -997,7 +970,7 @@ def __init__( if params: url = url.extend_query(params) - super().__init__(method, url, headers=headers, auth=auth, loop=loop, ssl=ssl) + super().__init__(method, url, headers=headers, loop=loop, ssl=ssl) if proxy is not None: assert type(proxy) is URL, proxy @@ -1011,7 +984,7 @@ def __init__( self._update_auto_headers(skip_auto_headers) self._update_cookies(cookies) self._update_content_encoding(data, compress) - self._update_proxy(proxy, proxy_auth, proxy_headers) + self._update_proxy(proxy, proxy_headers) self._update_body_from_data(data) if data is not None or self.method not in self.GET_METHODS: @@ -1042,7 +1015,6 @@ def connection_key(self) -> ConnectionKey: url.scheme in _SSL_SCHEMES, self._ssl, self.proxy, - self.proxy_auth, h, ), ) @@ -1273,18 +1245,20 @@ def _update_expect_continue(self, expect: bool = False) -> None: def _update_proxy( self, proxy: URL | None, - proxy_auth: BasicAuth | None, proxy_headers: CIMultiDict[str] | None, ) -> None: - self.proxy = proxy if proxy is None: - self.proxy_auth = None + self.proxy = None self.proxy_headers = None return - - if proxy_auth and not isinstance(proxy_auth, BasicAuth): - raise ValueError("proxy_auth must be None or BasicAuth() tuple") - self.proxy_auth = proxy_auth + # URL-embedded credentials on the proxy map to Proxy-Authorization. + if proxy.raw_user or proxy.raw_password: + auth_header = encode_basic_auth(proxy.user or "", proxy.password or "") + if proxy_headers is None: + proxy_headers = CIMultiDict() + proxy_headers.setdefault(hdrs.PROXY_AUTHORIZATION, auth_header) + proxy = proxy.with_user(None) + self.proxy = proxy self.proxy_headers = proxy_headers def _create_response(self, task: asyncio.Task[None] | None) -> ClientResponse: diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 547f9719d39..3f73b1f6418 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -562,16 +562,16 @@ def _update_proxy_auth_header_and_build_proxy_req( hdrs.METH_GET, url, headers=headers, - auth=req.proxy_auth, loop=self._loop, ssl=req.ssl, ) - auth = proxy_req.headers.pop(hdrs.AUTHORIZATION, None) - if auth is not None: - if not req.is_ssl(): - req.headers[hdrs.PROXY_AUTHORIZATION] = auth - else: - proxy_req.headers[hdrs.PROXY_AUTHORIZATION] = auth + if not req.is_ssl(): + # For non-SSL proxies the request goes directly through the proxy, + # so any Proxy-Authorization belongs on the request itself, not on + # the synthetic proxy request used for SSL CONNECT. + proxy_auth = proxy_req.headers.pop(hdrs.PROXY_AUTHORIZATION, None) + if proxy_auth is not None: + req.headers[hdrs.PROXY_AUTHORIZATION] = proxy_auth return proxy_req async def connect( @@ -1520,9 +1520,7 @@ async def _create_proxy_connection( # asyncio handles this perfectly proxy_req.method = hdrs.METH_CONNECT proxy_req.url = req.url - key = req.connection_key._replace( - proxy=None, proxy_auth=None, proxy_headers_hash=None - ) + key = req.connection_key._replace(proxy=None, proxy_headers_hash=None) conn = _ConnectTunnelConnection(self, key, proto, self._loop) proxy_resp = await proxy_req._send(conn) try: diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index bb07a8f79a2..55a5e01edcc 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -2,7 +2,6 @@ import asyncio import base64 -import binascii import contextlib import dataclasses import datetime @@ -17,7 +16,6 @@ import time import warnings import weakref -from collections import namedtuple from collections.abc import Callable, Iterable, Iterator, Mapping from contextlib import suppress from email.message import EmailMessage @@ -33,7 +31,6 @@ Any, ContextManager, Generic, - Optional, Protocol, TypeVar, Union, @@ -64,7 +61,7 @@ dataclasses.dataclass, frozen=True, slots=True ) -__all__ = ("BasicAuth", "ChainMapProxy", "ETag", "frozen_dataclass_decorator", "reify") +__all__ = ("ChainMapProxy", "ETag", "frozen_dataclass_decorator", "reify") # This is the default size/limit for several operations. # Matches the max size we receive from sockets: @@ -165,93 +162,17 @@ def encode_basic_auth(login: str, password: str = "", encoding: str = "utf-8") - return "Basic " + base64.b64encode(creds).decode(encoding) -class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])): - """Http basic authentication helper.""" +def strip_auth_from_url(url: URL) -> tuple[URL, str | None]: + """Strip user/password from a URL and return the Authorization header value. - def __new__( - cls, login: str, password: str = "", encoding: str = "latin1" - ) -> "BasicAuth": - if login is None: - raise ValueError("None is not allowed as login value") - - if password is None: - raise ValueError("None is not allowed as password value") - - if ":" in login: - raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)') - - warnings.warn( - "BasicAuth is deprecated and will be removed in aiohttp 4.0; " - "use aiohttp.encode_basic_auth() with " - "headers={'Authorization': ...} instead", - DeprecationWarning, - stacklevel=2, - ) - return super().__new__(cls, login, password, encoding) - - @classmethod - def decode(cls, auth_header: str, encoding: str = "latin1") -> "BasicAuth": - """Create a BasicAuth object from an Authorization HTTP header.""" - try: - auth_type, encoded_credentials = auth_header.split(" ", 1) - except ValueError: - raise ValueError("Could not parse authorization header.") - - if auth_type.lower() != "basic": - raise ValueError("Unknown authorization method %s" % auth_type) - - try: - decoded = base64.b64decode( - encoded_credentials.encode("ascii"), validate=True - ).decode(encoding) - except binascii.Error: - raise ValueError("Invalid base64 encoding.") - - try: - # RFC 2617 HTTP Authentication - # https://www.ietf.org/rfc/rfc2617.txt - # the colon must be present, but the username and password may be - # otherwise blank. - username, password = decoded.split(":", 1) - except ValueError: - raise ValueError("Invalid credentials.") - - return _basic_auth_no_warn(username, password, encoding) - - @classmethod - def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"]: - """Create BasicAuth from url.""" - if not isinstance(url, URL): - raise TypeError("url should be yarl.URL instance") - # Check raw_user and raw_password first as yarl is likely - # to already have these values parsed from the netloc in the cache. - if url.raw_user is None and url.raw_password is None: - return None - return _basic_auth_no_warn(url.user or "", url.password or "", encoding) - - def encode(self) -> str: - """Encode credentials.""" - return encode_basic_auth(self.login, self.password, self.encoding) - - -def _basic_auth_no_warn( - login: str, password: str = "", encoding: str = "latin1" -) -> BasicAuth: - """Construct a BasicAuth without emitting the deprecation warning. - - For internal use only. Bypasses BasicAuth.__new__ so that aiohttp's own - machinery doesn't trigger deprecation warnings in user code. + Returns a tuple of ``(url_without_credentials, authorization_header_value)``. + The header value is ``None`` if no credentials were present. """ - return tuple.__new__(BasicAuth, (login, password, encoding)) - - -def strip_auth_from_url(url: URL) -> tuple[URL, BasicAuth | None]: - """Remove user and password from URL if present and return BasicAuth object.""" # Check raw_user and raw_password first as yarl is likely # to already have these values parsed from the netloc in the cache. if url.raw_user is None and url.raw_password is None: return url, None - return url.with_user(None), _basic_auth_no_warn(url.user or "", url.password or "") + return url.with_user(None), encode_basic_auth(url.user or "", url.password or "") def netrc_from_env() -> netrc.netrc | None: @@ -302,12 +223,11 @@ def netrc_from_env() -> netrc.netrc | None: @frozen_dataclass_decorator class ProxyInfo: proxy: URL - proxy_auth: BasicAuth | None + proxy_auth: str | None -def basicauth_from_netrc(netrc_obj: netrc.netrc | None, host: str) -> BasicAuth: - """ - Return :py:class:`~aiohttp.BasicAuth` credentials for ``host`` from ``netrc_obj``. +def _auth_header_from_netrc(netrc_obj: netrc.netrc | None, host: str) -> str: + """Return a ``Proxy-Authorization`` header value for ``host`` from netrc. :raises LookupError: if ``netrc_obj`` is :py:data:`None` or if no entry is found for the ``host``. @@ -331,7 +251,7 @@ def basicauth_from_netrc(netrc_obj: netrc.netrc | None, host: str) -> BasicAuth: if password is None: password = "" # type: ignore[unreachable] - return _basic_auth_no_warn(username, password) + return encode_basic_auth(username, password) def proxies_from_env() -> dict[str, ProxyInfo]: @@ -353,14 +273,14 @@ def proxies_from_env() -> dict[str, ProxyInfo]: if netrc_obj and auth is None: if proxy.host is not None: try: - auth = basicauth_from_netrc(netrc_obj, proxy.host) + auth = _auth_header_from_netrc(netrc_obj, proxy.host) except LookupError: auth = None ret[proto] = ProxyInfo(proxy, auth) return ret -def get_env_proxy_for_url(url: URL) -> tuple[URL, BasicAuth | None]: +def get_env_proxy_for_url(url: URL) -> tuple[URL, str | None]: """Get a permitted proxy for the given URL from the env.""" if url.host is not None and proxy_bypass(url.host): raise LookupError(f"Proxying is disallowed for `{url.host!r}`") diff --git a/docs/client_advanced.rst b/docs/client_advanced.rst index bfea3295d1a..6d370351cad 100644 --- a/docs/client_advanced.rst +++ b/docs/client_advanced.rst @@ -68,12 +68,6 @@ For HTTP Basic Authentication, build the ``Authorization`` header using async with ClientSession(headers=headers) as session: ... -.. deprecated:: 3.14 - - The ``auth`` parameter and the :class:`BasicAuth` class are deprecated and - will be removed in 4.0. Use :func:`encode_basic_auth` together with the - ``headers`` parameter as shown above. - For HTTP digest authentication, use the :class:`DigestAuthMiddleware` client middleware:: from aiohttp import ClientSession, DigestAuthMiddleware @@ -748,12 +742,6 @@ And you may set default proxy:: async with session.get("http://python.org") as resp: print(resp.status) -.. deprecated:: 3.14 - - The ``proxy_auth`` parameter is deprecated and will be removed in 4.0. Use - :func:`encode_basic_auth` with ``proxy_headers={"Proxy-Authorization": ...}`` - as shown above. - Contrary to the ``requests`` library, it won't read environment variables by default. But you can do so by passing ``trust_env=True`` into :class:`aiohttp.ClientSession` diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 657b463a1a8..47d50df3dae 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -40,7 +40,7 @@ The client session supports the context manager protocol for self closing. .. class:: ClientSession(base_url=None, *, \ connector=None, cookies=None, \ headers=None, skip_auto_headers=None, \ - auth=None, json_serialize=json.dumps, \ + json_serialize=json.dumps, \ request_class=ClientRequest, \ response_class=ClientResponse, \ ws_response_class=ClientWebSocketResponse, \ @@ -110,14 +110,6 @@ The client session supports the context manager protocol for self closing. Iterable of :class:`str` or :class:`~multidict.istr` (optional) - :param aiohttp.BasicAuth auth: an object that represents HTTP Basic - Authorization (optional). It will be included - with any request. However, if the - ``_base_url`` parameter is set, the request - URL's origin must match the base URL's origin; - otherwise, the default auth will not be - included. - :param collections.abc.Callable json_serialize: Json *serializer* callable. By default :func:`json.dumps` function. @@ -340,14 +332,6 @@ The client session supports the context manager protocol for self closing. .. versionadded:: 3.7 - .. attribute:: auth - - An object that represents HTTP Basic Authorization. - - :class:`~aiohttp.BasicAuth` (optional) - - .. versionadded:: 3.7 - .. attribute:: json_serialize Json serializer callable. @@ -400,11 +384,11 @@ The client session supports the context manager protocol for self closing. .. method:: request(method, url, *, params=None, data=None, json=None,\ cookies=None, headers=None, skip_auto_headers=None, \ - auth=None, allow_redirects=True,\ + allow_redirects=True,\ max_redirects=10,\ compress=None, chunked=None, expect100=False, raise_for_status=None,\ read_until_eof=True, \ - proxy=None, proxy_auth=None,\ + proxy=None,\ timeout=sentinel, ssl=True, \ server_hostname=None, \ proxy_headers=None, \ @@ -473,9 +457,6 @@ The client session supports the context manager protocol for self closing. Iterable of :class:`str` or :class:`~multidict.istr` (optional) - :param aiohttp.BasicAuth auth: an object that represents HTTP - Basic Authorization (optional) - :param bool allow_redirects: Whether to process redirects or not. When ``True``, redirects are followed (up to ``max_redirects`` times) and logged into :attr:`ClientResponse.history` and ``trace_configs``. @@ -515,9 +496,6 @@ The client session supports the context manager protocol for self closing. :param proxy: Proxy URL, :class:`str` or :class:`~yarl.URL` (optional) - :param aiohttp.BasicAuth proxy_auth: an object that represents proxy HTTP - Basic Authorization (optional) - :param int timeout: override the session's timeout. .. versionchanged:: 3.3 @@ -719,14 +697,13 @@ The client session supports the context manager protocol for self closing. .. method:: ws_connect(url, *, method='GET', \ protocols=(), \ timeout=sentinel,\ - auth=None,\ autoclose=True,\ autoping=True,\ heartbeat=None,\ origin=None, \ params=None, \ headers=None, \ - proxy=None, proxy_auth=None, ssl=True, \ + proxy=None, ssl=True, \ verify_ssl=None, fingerprint=None, \ ssl_context=None, proxy_headers=None, \ compress=0, max_msg_size=4194304, \ @@ -748,9 +725,6 @@ The client session supports the context manager protocol for self closing. (``10.0`` seconds for the websocket to close). ``None`` means no timeout will be used. - :param aiohttp.BasicAuth auth: an object that represents HTTP - Basic Authorization (optional) - :param bool autoclose: Automatically close websocket connection on close message from server. If *autoclose* is False then close procedure has to be handled manually. @@ -788,9 +762,6 @@ The client session supports the context manager protocol for self closing. :param str proxy: Proxy URL, :class:`str` or :class:`~yarl.URL` (optional) - :param aiohttp.BasicAuth proxy_auth: an object that represents proxy HTTP - Basic Authorization (optional) - :param ssl: SSL validation mode. ``True`` for default SSL check (:func:`ssl.create_default_context` is used), ``False`` for skip SSL certificate validation, @@ -897,11 +868,11 @@ certification chaining. .. function:: request(method, url, *, params=None, data=None, \ json=None,\ - cookies=None, headers=None, skip_auto_headers=None, auth=None, \ + cookies=None, headers=None, skip_auto_headers=None, \ allow_redirects=True, max_redirects=10, \ compress=False, chunked=None, expect100=False, raise_for_status=None, \ read_until_eof=True, \ - proxy=None, proxy_auth=None, \ + proxy=None, \ timeout=sentinel, ssl=True, \ server_hostname=None, \ proxy_headers=None, \ @@ -964,9 +935,6 @@ certification chaining. Iterable of :class:`str` or :class:`~multidict.istr` (optional) - :param aiohttp.BasicAuth auth: an object that represents HTTP Basic - Authorization (optional) - :param bool allow_redirects: Whether to process redirects or not. When ``True``, redirects are followed (up to ``max_redirects`` times) and logged into :attr:`ClientResponse.history` and ``trace_configs``. @@ -1011,9 +979,6 @@ certification chaining. :param proxy: Proxy URL, :class:`str` or :class:`~yarl.URL` (optional) - :param aiohttp.BasicAuth proxy_auth: an object that represents proxy HTTP - Basic Authorization (optional) - :param timeout: a :class:`ClientTimeout` settings structure, 300 seconds (5min) total timeout, 30 seconds socket connect timeout by default. @@ -2342,55 +2307,6 @@ Utilities .. versionadded:: 3.14 -.. class:: BasicAuth(login, password='', encoding='latin1') - :canonical: aiohttp.helpers.BasicAuth - - HTTP basic authentication helper. - - :param str login: login - :param str password: password - :param str encoding: encoding (``'latin1'`` by default) - - - Previously this was used for specifying authorization data in client API, - e.g. *auth* parameter for :meth:`ClientSession.request() `. - - .. deprecated:: 3.14 - - Constructing :class:`BasicAuth` is deprecated and will be removed in - 4.0. Use :func:`encode_basic_auth` together with the ``headers`` - parameter (or ``proxy_headers`` for proxies) instead. The - :meth:`decode` and :meth:`from_url` class methods remain available for - parsing. - - - .. classmethod:: decode(auth_header, encoding='latin1') - - Decode HTTP basic authentication credentials. - - :param str auth_header: The ``Authorization`` header to decode. - :param str encoding: (optional) encoding ('latin1' by default) - - :return: decoded authentication data, :class:`BasicAuth`. - - .. classmethod:: from_url(url) - - Constructed credentials info from url's *user* and *password* - parts. - - :return: credentials data, :class:`BasicAuth` or ``None`` is - credentials are not provided. - - .. versionadded:: 2.3 - - .. method:: encode() - - Encode credentials into string suitable for ``Authorization`` - header etc. - - :return: encoded authentication data, :class:`str`. - - .. class:: DigestAuthMiddleware(login, password, *, preemptive=True) :canonical: aiohttp.client_middleware_digest_auth.DigestAuthMiddleware diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 352b2e29a64..036e33aa7b4 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -40,7 +40,6 @@ Backporting backports BaseEventLoop basename -BasicAuth behaviour BodyPartReader boolean diff --git a/examples/client_auth.py b/examples/client_auth.py index 248f67a48d6..b4357d1e047 100755 --- a/examples/client_auth.py +++ b/examples/client_auth.py @@ -13,9 +13,8 @@ async def fetch(session: aiohttp.ClientSession) -> None: async def go() -> None: - async with aiohttp.ClientSession( - auth=aiohttp.BasicAuth("andrew", "password") - ) as session: + headers = {"Authorization": aiohttp.encode_basic_auth("andrew", "password")} + async with aiohttp.ClientSession(headers=headers) as session: await fetch(session) diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index c11d33a7bda..51321ff12a3 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/base.txt b/requirements/base.txt index 04f4bd98e6b..b9ed37d6176 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 10c1d61adf3..5d9fdef148b 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in # -aiodns==4.0.0 +aiodns==4.0.3 # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -24,7 +24,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via @@ -59,7 +59,7 @@ cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 # via requests -click==8.3.3 +click==8.4.0 # via # pip-tools # slotscheck @@ -199,6 +199,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-aiohttp==1.1.0 # via @@ -216,6 +217,8 @@ pytest-mock==3.15.1 # via # -r requirements/lint.in # -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/requirements/dev.txt b/requirements/dev.txt index e7f10a52253..2253454c89a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in # -aiodns==4.0.0 +aiodns==4.0.3 # via # -r requirements/lint.in # -r requirements/runtime-deps.in @@ -24,7 +24,7 @@ alabaster==1.0.0 # via sphinx annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via @@ -59,7 +59,7 @@ cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.7 # via requests -click==8.3.3 +click==8.4.0 # via # pip-tools # slotscheck @@ -194,6 +194,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-aiohttp==1.1.0 # via @@ -211,6 +212,8 @@ pytest-mock==3.15.1 # via # -r requirements/lint.in # -r requirements/test-common.in +pytest-timeout==2.4.0 + # via -r requirements/test-common.in pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index ed58e6c876f..eb528786532 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -14,7 +14,7 @@ certifi==2026.4.22 # via requests charset-normalizer==3.4.7 # via requests -click==8.3.3 +click==8.4.0 # via towncrier docutils==0.21.2 # via sphinx diff --git a/requirements/doc.txt b/requirements/doc.txt index 2b92eb36c88..9ec45e626e2 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -14,7 +14,7 @@ certifi==2026.4.22 # via requests charset-normalizer==3.4.7 # via requests -click==8.3.3 +click==8.4.0 # via towncrier docutils==0.21.2 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index 4c70287b346..ca0b84160f7 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/lint.txt --strip-extras requirements/lint.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/lint.in aiohappyeyeballs==2.6.1 # via aiohttp @@ -14,7 +14,7 @@ aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 # via @@ -34,7 +34,7 @@ cffi==2.0.0 # pycares cfgv==3.5.0 # via pre-commit -click==8.3.3 +click==8.4.0 # via slotscheck cryptography==48.0.0 # via trustme diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index 07175fbdba3..4ab91518028 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --resolver=backtracking --strip-extras requirements/runtime-deps.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via -r requirements/runtime-deps.in diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 1faa93e7c1f..f21137a4ff5 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -12,7 +12,7 @@ aiosignal==1.4.0 # via aiohttp annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 # via aiohttp @@ -24,7 +24,7 @@ blockbuster==1.5.26 # via -r requirements/test-common.in cffi==2.0.0 # via cryptography -click==8.3.3 +click==8.4.0 # via wait-for-it coverage==7.14.0 # via @@ -100,6 +100,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-aiohttp==1.1.0 # via -r requirements/test-common.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 073a4360a15..bd5a648e30c 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via @@ -18,7 +18,7 @@ aiosignal==1.4.0 # aiohttp annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via @@ -38,7 +38,7 @@ cffi==2.0.0 # via # cryptography # pycares -click==8.3.3 +click==8.4.0 # via wait-for-it coverage==7.14.0 # via @@ -123,6 +123,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-aiohttp==1.1.0 # via -r requirements/test-common.in diff --git a/requirements/test.txt b/requirements/test.txt index 3c84d9304c1..ea891dcfb47 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in # -aiodns==4.0.0 +aiodns==4.0.3 # via -r requirements/runtime-deps.in aiohappyeyeballs==2.6.1 # via @@ -18,7 +18,7 @@ aiosignal==1.4.0 # aiohttp annotated-types==0.7.0 # via pydantic -ast-serialize==0.4.0 +ast-serialize==0.5.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via @@ -38,7 +38,7 @@ cffi==2.0.0 # via # cryptography # pycares -click==8.3.3 +click==8.4.0 # via wait-for-it coverage==7.14.0 # via @@ -123,6 +123,7 @@ pytest==9.0.3 # pytest-codspeed # pytest-cov # pytest-mock + # pytest-timeout # pytest-xdist pytest-aiohttp==1.1.0 # via -r requirements/test-common.in diff --git a/tests/conftest.py b/tests/conftest.py index a37b13ca309..931fc01e434 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -442,14 +442,12 @@ def maker( "skip_auto_headers": None, "data": None, "cookies": BaseCookie[str](), - "auth": None, "version": HttpVersion11, "compress": False, "chunked": None, "expect100": False, "response_class": ClientResponse, "proxy": None, - "proxy_auth": None, "timer": TimerNoop(), "session": session, "ssl": True, diff --git a/tests/test_benchmarks_client_request.py b/tests/test_benchmarks_client_request.py index 6d6f9fd58ab..dcc7c8c0b4e 100644 --- a/tests/test_benchmarks_client_request.py +++ b/tests/test_benchmarks_client_request.py @@ -63,7 +63,6 @@ def _run() -> None: skip_auto_headers=None, response_class=ClientResponse, proxy=None, - proxy_auth=None, proxy_headers=None, timer=timer, session=None, # type: ignore[arg-type] @@ -74,7 +73,6 @@ def _run() -> None: headers=headers, data=None, cookies=cookies, - auth=None, version=HttpVersion11, compress=False, chunked=None, @@ -102,7 +100,6 @@ def _run() -> None: skip_auto_headers=None, response_class=ClientResponse, proxy=None, - proxy_auth=None, proxy_headers=None, timer=timer, session=None, # type: ignore[arg-type] @@ -113,7 +110,6 @@ def _run() -> None: headers=headers, data=None, cookies=cookies, - auth=None, version=HttpVersion11, compress=False, chunked=None, diff --git a/tests/test_client_exceptions.py b/tests/test_client_exceptions.py index b95325fb152..2c617013c8d 100644 --- a/tests/test_client_exceptions.py +++ b/tests/test_client_exceptions.py @@ -93,7 +93,6 @@ class TestClientConnectorError: is_ssl=False, ssl=True, proxy=None, - proxy_auth=None, proxy_headers_hash=None, ) @@ -154,7 +153,6 @@ class TestClientConnectorCertificateError: is_ssl=False, ssl=True, proxy=None, - proxy_auth=None, proxy_headers_hash=None, ) diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 10953516a0a..6f554250805 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -65,16 +65,6 @@ from aiohttp.test_utils import TestClient, TestServer from aiohttp.typedefs import Handler -pytestmark = [ - pytest.mark.filterwarnings(r"ignore:BasicAuth is deprecated:DeprecationWarning"), - pytest.mark.filterwarnings( - r"ignore:The 'auth' parameter is deprecated:DeprecationWarning" - ), - pytest.mark.filterwarnings( - r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning" - ), -] - @pytest.fixture def here() -> pathlib.Path: @@ -3306,11 +3296,12 @@ async def test_invalid_idna() -> None: await session.get("http://\u0080owhefopw.com") -async def test_creds_in_auth_and_url() -> None: +async def test_creds_in_header_and_url() -> None: async with aiohttp.ClientSession() as session: with pytest.raises(ValueError): await session.get( - "http://user:pass@example.com", auth=aiohttp.BasicAuth("user2", "pass2") + "http://user:pass@example.com", + headers={"Authorization": aiohttp.encode_basic_auth("user2", "pass2")}, ) @@ -3367,14 +3358,17 @@ async def close(self) -> None: async with ( aiohttp.ClientSession(connector=connector) as client, - client.get(url_from, auth=aiohttp.BasicAuth("user", "pass")) as resp, + client.get( + url_from, + headers={"Authorization": aiohttp.encode_basic_auth("user", "pass")}, + ) as resp, ): assert len(resp.history) == 1 assert str(resp.url) == "http://example.com" assert resp.status == 200 assert ( resp.request_info.headers.get("authorization") == "Basic dXNlcjo=" - ), "Expected redirect credentials to take precedence over provided auth" + ), "Expected redirect credentials to take precedence over the Authorization header" @pytest.fixture @@ -3475,8 +3469,11 @@ async def close(self) -> None: async with aiohttp.ClientSession(connector=connector) as client: async with client.get( url_from, - auth=aiohttp.BasicAuth("user", "pass"), - headers={"Proxy-Authorization": "Basic dXNlcjpwYXNz", "Cookie": "a=b"}, + headers={ + "Authorization": "Basic dXNlcjpwYXNz", + "Proxy-Authorization": "Basic dXNlcjpwYXNz", + "Cookie": "a=b", + }, ) as resp: assert resp.status == 200 async with client.get( @@ -3490,74 +3487,10 @@ async def close(self) -> None: assert resp.status == 200 -async def test_auth_persist_on_redirect_to_other_host_with_global_auth( - create_server_for_url_and_handler: Callable[[URL, Handler], Awaitable[TestServer]], -) -> None: - url_from = URL("http://host1.com/path1") - url_to = URL("http://host2.com/path2") - - async def srv_from(request: web.Request) -> NoReturn: - assert request.host == url_from.host - assert request.headers["Authorization"] == "Basic dXNlcjpwYXNz" - raise web.HTTPFound(url_to) - - async def srv_to(request: web.Request) -> web.Response: - assert request.host == url_to.host - assert "Authorization" in request.headers, "Header was dropped" - return web.Response() - - server_from = await create_server_for_url_and_handler(url_from, srv_from) - server_to = await create_server_for_url_and_handler(url_to, srv_to) - - assert ( - url_from.host != url_to.host or server_from.scheme != server_to.scheme - ), "Invalid test case, host or scheme must differ" - - protocol_port_map = { - "http": 80, - "https": 443, - } - etc_hosts = { - (url_from.host, protocol_port_map[server_from.scheme]): server_from, - (url_to.host, protocol_port_map[server_to.scheme]): server_to, - } - - class FakeResolver(AbstractResolver): - async def resolve( - self, - host: str, - port: int = 0, - family: socket.AddressFamily = socket.AF_INET, - ) -> list[ResolveResult]: - server = etc_hosts[(host, port)] - assert server.port is not None - - return [ - { - "hostname": host, - "host": server.host, - "port": server.port, - "family": socket.AF_INET, - "proto": 0, - "flags": socket.AI_NUMERICHOST, - } - ] - - async def close(self) -> None: - """Dummy""" - - connector = aiohttp.TCPConnector(resolver=FakeResolver(), ssl=False) - - async with aiohttp.ClientSession( - connector=connector, auth=aiohttp.BasicAuth("user", "pass") - ) as client: - async with client.get(url_from) as resp: - assert resp.status == 200 - - -async def test_drop_auth_on_redirect_to_other_host_with_global_auth_and_base_url( +async def test_drop_session_authorization_header_on_redirect_to_other_host( create_server_for_url_and_handler: Callable[[URL, Handler], Awaitable[TestServer]], ) -> None: + """Authorization header from ClientSession(headers=...) is dropped on cross-origin redirect.""" url_from = URL("http://host1.com/path1") url_to = URL("http://host2.com/path2") @@ -3615,10 +3548,9 @@ async def close(self) -> None: async with aiohttp.ClientSession( connector=connector, - base_url="http://host1.com", - auth=aiohttp.BasicAuth("user", "pass"), + headers={"Authorization": "Basic dXNlcjpwYXNz"}, ) as client: - async with client.get("/path1") as resp: + async with client.get(url_from) as resp: assert resp.status == 200 @@ -3831,12 +3763,14 @@ async def handler(request: web.Request) -> web.Response: assert resp.status == 200 -async def test_session_auth( +async def test_session_authorization_header( headers_echo_client: Callable[ ..., Awaitable[TestClient[web.Request, web.Application]] ], ) -> None: - client = await headers_echo_client(auth=aiohttp.BasicAuth("login", "pass")) + client = await headers_echo_client( + headers={"Authorization": aiohttp.encode_basic_auth("login", "pass")} + ) async with client.get("/") as r: assert r.status == 200 @@ -3844,96 +3778,23 @@ async def test_session_auth( assert content["headers"]["Authorization"] == "Basic bG9naW46cGFzcw==" -async def test_session_auth_override( - headers_echo_client: Callable[ - ..., Awaitable[TestClient[web.Request, web.Application]] - ], -) -> None: - client = await headers_echo_client(auth=aiohttp.BasicAuth("login", "pass")) - - async with client.get("/", auth=aiohttp.BasicAuth("other_login", "pass")) as r: - assert r.status == 200 - content = await r.json() - val = content["headers"]["Authorization"] - assert val == "Basic b3RoZXJfbG9naW46cGFzcw==" - - -async def test_session_auth_header_conflict(aiohttp_client: AiohttpClient) -> None: - async def handler(request: web.Request) -> NoReturn: - assert False - - app = web.Application() - app.router.add_get("/", handler) - - client = await aiohttp_client(app, auth=aiohttp.BasicAuth("login", "pass")) - headers = {"Authorization": "Basic b3RoZXJfbG9naW46cGFzcw=="} - with pytest.raises(ValueError): - await client.get("/", headers=headers) - - -@pytest.mark.usefixtures("netrc_default_contents") -async def test_netrc_auth_from_env( # type: ignore[misc] +async def test_session_authorization_header_override( headers_echo_client: Callable[ ..., Awaitable[TestClient[web.Request, web.Application]] ], ) -> None: - """Test that netrc authentication works when NETRC env var is set and trust_env=True.""" - client = await headers_echo_client(trust_env=True) - async with client.get("/") as r: - assert r.status == 200 - content = await r.json() - # Base64 encoded "netrc_user:netrc_pass" is "bmV0cmNfdXNlcjpuZXRyY19wYXNz" - assert content["headers"]["Authorization"] == "Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" - - -@pytest.mark.usefixtures("no_netrc") -async def test_netrc_auth_skipped_without_netrc_file( # type: ignore[misc] - headers_echo_client: Callable[ - ..., Awaitable[TestClient[web.Request, web.Application]] - ], -) -> None: - """Test that netrc authentication is skipped when no netrc file exists.""" - client = await headers_echo_client(trust_env=True) - async with client.get("/") as r: - assert r.status == 200 - content = await r.json() - # No Authorization header should be present - assert "Authorization" not in content["headers"] - - -@pytest.mark.usefixtures("netrc_home_directory") -async def test_netrc_auth_from_home_directory( # type: ignore[misc] - headers_echo_client: Callable[ - ..., Awaitable[TestClient[web.Request, web.Application]] - ], -) -> None: - """Test that netrc authentication works from default ~/.netrc without NETRC env var.""" - client = await headers_echo_client(trust_env=True) - async with client.get("/") as r: - assert r.status == 200 - content = await r.json() - assert content["headers"]["Authorization"] == "Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" - + client = await headers_echo_client( + headers={"Authorization": aiohttp.encode_basic_auth("login", "pass")} + ) -@pytest.mark.usefixtures("netrc_default_contents") -async def test_netrc_auth_overridden_by_explicit_auth( # type: ignore[misc] - headers_echo_client: Callable[ - ..., Awaitable[TestClient[web.Request, web.Application]] - ], -) -> None: - """Test that explicit auth parameter overrides netrc authentication.""" - client = await headers_echo_client(trust_env=True) - # Make request with explicit auth (should override netrc) async with client.get( - "/", auth=aiohttp.BasicAuth("explicit_user", "explicit_pass") + "/", + headers={"Authorization": aiohttp.encode_basic_auth("other_login", "pass")}, ) as r: assert r.status == 200 content = await r.json() - # Base64 encoded "explicit_user:explicit_pass" is "ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" - assert ( - content["headers"]["Authorization"] - == "Basic ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" - ) + val = content["headers"]["Authorization"] + assert val == "Basic b3RoZXJfbG9naW46cGFzcw==" async def test_session_headers( diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 08d075bbba4..05a0f33d7ba 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -347,7 +347,7 @@ async def test_host_header_ipv6_with_port(make_client_request: _RequestMaker) -> ), ), ) -async def test_host_header_fqdn( # type: ignore[misc] +async def test_host_header_fqdn( make_client_request: _RequestMaker, url: str, headers: CIMultiDict[str], @@ -456,27 +456,6 @@ async def test_ipv6_nondefault_https_port(make_client_request: _RequestMaker) -> assert req.is_ssl() -async def test_basic_auth(make_client_request: _RequestMaker) -> None: - with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): - auth = aiohttp.BasicAuth("nkim", "1234") - req = make_client_request("get", URL("http://python.org"), auth=auth) - assert "AUTHORIZATION" in req.headers - assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"] - - -async def test_basic_auth_utf8(make_client_request: _RequestMaker) -> None: - with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): - auth = aiohttp.BasicAuth("nkim", "секрет", "utf-8") - req = make_client_request("get", URL("http://python.org"), auth=auth) - assert "AUTHORIZATION" in req.headers - assert "Basic bmtpbTrRgdC10LrRgNC10YI=" == req.headers["AUTHORIZATION"] - - -async def test_basic_auth_tuple_forbidden(make_client_request: _RequestMaker) -> None: - with pytest.raises(TypeError): - make_client_request("get", URL("http://python.org"), auth=("nkim", "1234")) # type: ignore[arg-type] - - async def test_basic_auth_from_url(make_client_request: _RequestMaker) -> None: req = make_client_request("get", URL("http://nkim:1234@python.org")) assert "AUTHORIZATION" in req.headers @@ -491,15 +470,16 @@ async def test_basic_auth_no_user_from_url(make_client_request: _RequestMaker) - assert "python.org" == req.url.host -async def test_basic_auth_from_url_overridden( +async def test_basic_auth_from_url_overrides_authorization_header( make_client_request: _RequestMaker, ) -> None: - with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): - auth = aiohttp.BasicAuth("nkim", "1234") - req = make_client_request("get", URL("http://garbage@python.org"), auth=auth) - assert "AUTHORIZATION" in req.headers - assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"] - assert "python.org" == req.url.host + req = make_client_request( + "get", + URL("http://nkim:1234@python.org"), + headers=CIMultiDict({"AUTHORIZATION": "Basic Z2FyYmFnZQ=="}), + ) + assert req.headers["AUTHORIZATION"] == "Basic bmtpbToxMjM0" + assert req.url.host == "python.org" async def test_path_is_not_double_encoded1(make_client_request: _RequestMaker) -> None: @@ -938,7 +918,7 @@ async def test_bytes_data(conn: mock.Mock, make_client_request: _RequestMaker) - @pytest.mark.usefixtures("parametrize_zlib_backend") -async def test_content_encoding( # type: ignore[misc] +async def test_content_encoding( conn: mock.Mock, make_client_request: _RequestMaker ) -> None: loop = asyncio.get_running_loop() @@ -987,7 +967,7 @@ async def test_content_encoding_rejects_unknown_string( @pytest.mark.usefixtures("parametrize_zlib_backend") -async def test_content_encoding_header( # type: ignore[misc] +async def test_content_encoding_header( conn: mock.Mock, make_client_request: _RequestMaker ) -> None: req = make_client_request( @@ -1121,7 +1101,7 @@ async def test_file_upload_not_chunked(make_client_request: _RequestMaker) -> No @pytest.mark.usefixtures("parametrize_zlib_backend") -async def test_precompressed_data_stays_intact( # type: ignore[misc] +async def test_precompressed_data_stays_intact( make_client_request: _RequestMaker, ) -> None: data = ZLibBackend.compress(b"foobar") @@ -1504,7 +1484,7 @@ async def test_oserror_on_write_bytes( @pytest.mark.skipif(sys.version_info < (3, 11), reason="Needs Task.cancelling()") -async def test_cancel_close( # type: ignore[misc] +async def test_cancel_close( conn: mock.Mock, make_client_request: _RequestMaker ) -> None: loop = asyncio.get_running_loop() @@ -1568,14 +1548,12 @@ async def go() -> None: skip_auto_headers=None, data=None, cookies=BaseCookie[str](), - auth=None, version=HttpVersion11, compress=False, chunked=None, expect100=False, response_class=ClientResponse, proxy=None, - proxy_auth=None, timer=TimerNoop(), session=None, # type: ignore[arg-type] ssl=True, @@ -1715,7 +1693,7 @@ def test_gen_default_accept_encoding( indirect=("netrc_contents",), ) @pytest.mark.usefixtures("netrc_contents") -async def test_basicauth_from_netrc_present_untrusted_env( # type: ignore[misc] +async def test_basicauth_from_netrc_present_untrusted_env( make_client_request: _RequestMaker, ) -> None: """Test no authorization header is sent via netrc if trust_env is False""" @@ -1729,7 +1707,7 @@ async def test_basicauth_from_netrc_present_untrusted_env( # type: ignore[misc] indirect=("netrc_contents",), ) @pytest.mark.usefixtures("netrc_contents") -async def test_basicauth_from_empty_netrc( # type: ignore[misc] +async def test_basicauth_from_empty_netrc( make_client_request: _RequestMaker, ) -> None: """Test that no Authorization header is sent when netrc is empty""" @@ -1873,7 +1851,7 @@ async def test_write_bytes_with_content_length_limit( b"Part1Part2Part3", ], ) -async def test_write_bytes_with_iterable_content_length_limit( # type: ignore[misc] +async def test_write_bytes_with_iterable_content_length_limit( buf: bytearray, conn: mock.Mock, data: list[bytes] | bytes, @@ -2098,7 +2076,7 @@ async def test_expect100_with_body_becomes_empty( ("DELETE", b"x", "1"), ], ) -async def test_content_length_for_methods( # type: ignore[misc] +async def test_content_length_for_methods( method: str, data: bytes | None, expected_content_length: str | None, @@ -2202,7 +2180,7 @@ async def test_no_content_length_with_chunked( @pytest.mark.parametrize("method", ["POST", "PUT", "PATCH", "DELETE"]) -async def test_update_body_none_sets_content_length_zero( # type: ignore[misc] +async def test_update_body_none_sets_content_length_zero( method: str, make_client_request: _RequestMaker ) -> None: """Test that updating body to None sets Content-Length: 0 for POST-like methods.""" @@ -2220,7 +2198,7 @@ async def test_update_body_none_sets_content_length_zero( # type: ignore[misc] @pytest.mark.parametrize("method", ["GET", "HEAD", "OPTIONS", "TRACE"]) -async def test_update_body_none_no_content_length_for_get_methods( # type: ignore[misc] +async def test_update_body_none_no_content_length_for_get_methods( method: str, make_client_request: _RequestMaker ) -> None: """Test that updating body to None doesn't set Content-Length for GET-like methods.""" diff --git a/tests/test_client_session.py b/tests/test_client_session.py index dc46b20fd05..910ee7d87f6 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -57,7 +57,7 @@ async def connector( async def make_conn() -> BaseConnector: return BaseConnector() - key = ConnectionKey("localhost", 80, False, True, None, None, None) + key = ConnectionKey("localhost", 80, False, True, None, None) conn = await make_conn() proto = create_mocked_conn() conn._conns[key] = deque([(proto, 123)]) @@ -892,54 +892,34 @@ async def test_proxy_str(session: ClientSession, params: _Params) -> None: ] -@pytest.mark.filterwarnings( - r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning" -) async def test_default_proxy() -> None: proxy_url = URL("http://proxy.example.com") - proxy_auth = mock.Mock() proxy_url2 = URL("http://proxy.example2.com") - proxy_auth2 = mock.Mock() class OnCall(Exception): pass request_class_mock = mock.Mock(side_effect=OnCall()) - session = ClientSession( - proxy=proxy_url, proxy_auth=proxy_auth, request_class=request_class_mock - ) + session = ClientSession(proxy=proxy_url, request_class=request_class_mock) assert session._default_proxy == proxy_url, "`ClientSession._default_proxy` not set" - assert ( - session._default_proxy_auth == proxy_auth - ), "`ClientSession._default_proxy_auth` not set" with pytest.raises(OnCall): - await session.get( - "http://example.com", - ) + await session.get("http://example.com") assert request_class_mock.called, "request class not called" assert ( request_class_mock.call_args[1].get("proxy") == proxy_url ), "`ClientSession._request` uses default proxy not one used in ClientSession.get" - assert ( - request_class_mock.call_args[1].get("proxy_auth") == proxy_auth - ), "`ClientSession._request` uses default proxy_auth not one used in ClientSession.get" request_class_mock.reset_mock() with pytest.raises(OnCall): - await session.get( - "http://example.com", proxy=proxy_url2, proxy_auth=proxy_auth2 - ) + await session.get("http://example.com", proxy=proxy_url2) assert request_class_mock.called, "request class not called" assert ( request_class_mock.call_args[1].get("proxy") == proxy_url2 - ), "`ClientSession._request` uses default proxy not one used in ClientSession.get" - assert ( - request_class_mock.call_args[1].get("proxy_auth") == proxy_auth2 - ), "`ClientSession._request` uses default proxy_auth not one used in ClientSession.get" + ), "`ClientSession._request` uses per-request proxy not session default" await session.close() @@ -1438,7 +1418,6 @@ async def test_instantiation_with_invalid_timeout_value() -> None: ("outer_name", "inner_name"), [ ("skip_auto_headers", "_skip_auto_headers"), - ("auth", "_default_auth"), ("json_serialize", "_json_serialize"), ("connector_owner", "_connector_owner"), ("raise_for_status", "_raise_for_status"), @@ -1500,17 +1479,14 @@ async def test_netrc_auth_from_home_directory(auth_server: TestServer) -> None: @pytest.mark.usefixtures("netrc_default_contents") -@pytest.mark.filterwarnings( - r"ignore:The 'auth' parameter is deprecated:DeprecationWarning", - r"ignore:BasicAuth is deprecated:DeprecationWarning", -) async def test_netrc_auth_overridden_by_explicit_auth(auth_server: TestServer) -> None: """Test that explicit auth parameter overrides netrc authentication.""" + explicit = aiohttp.encode_basic_auth("explicit_user", "explicit_pass") async with ( ClientSession(trust_env=True) as session, session.get( auth_server.make_url("/"), - auth=aiohttp.BasicAuth("explicit_user", "explicit_pass"), + headers={"Authorization": explicit}, ) as resp, ): text = await resp.text() @@ -1518,24 +1494,6 @@ async def test_netrc_auth_overridden_by_explicit_auth(auth_server: TestServer) - assert text == "auth:Basic ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz" -async def test_client_session_auth_deprecated() -> None: - """ClientSession(auth=...) emits a DeprecationWarning.""" - with pytest.warns(DeprecationWarning, match="'auth' parameter is deprecated"): - session = ClientSession( - auth=aiohttp.helpers._basic_auth_no_warn("user", "pass") - ) - await session.close() - - -async def test_client_session_proxy_auth_deprecated() -> None: - """ClientSession(proxy_auth=...) emits a DeprecationWarning.""" - with pytest.warns(DeprecationWarning, match="'proxy_auth' parameter is deprecated"): - session = ClientSession( - proxy_auth=aiohttp.helpers._basic_auth_no_warn("user", "pass") - ) - await session.close() - - @pytest.mark.usefixtures("netrc_other_host") async def test_netrc_auth_host_not_in_netrc(auth_server: TestServer) -> None: """Test that netrc lookup returns None when host is not in netrc file.""" diff --git a/tests/test_connector.py b/tests/test_connector.py index 1019437bd0d..3b73677cbc6 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -65,25 +65,25 @@ @pytest.fixture def key() -> ConnectionKey: # Connection key - return ConnectionKey("localhost", 80, False, True, None, None, None) + return ConnectionKey("localhost", 80, False, True, None, None) @pytest.fixture def key2() -> ConnectionKey: # Connection key - return ConnectionKey("localhost", 80, False, True, None, None, None) + return ConnectionKey("localhost", 80, False, True, None, None) @pytest.fixture def other_host_key2() -> ConnectionKey: # Connection key - return ConnectionKey("otherhost", 80, False, True, None, None, None) + return ConnectionKey("otherhost", 80, False, True, None, None) @pytest.fixture def ssl_key() -> ConnectionKey: # Connection key - return ConnectionKey("localhost", 80, True, True, None, None, None) + return ConnectionKey("localhost", 80, True, True, None, None) @pytest.fixture @@ -233,7 +233,7 @@ async def test_del(key: ConnectionKey) -> None: @pytest.mark.xfail -async def test_del_with_scheduled_cleanup(key: ConnectionKey) -> None: # type: ignore[misc] +async def test_del_with_scheduled_cleanup(key: ConnectionKey) -> None: loop = asyncio.get_running_loop() loop.set_debug(True) conn = aiohttp.BaseConnector(keepalive_timeout=0.01) @@ -262,7 +262,7 @@ async def test_del_with_scheduled_cleanup(key: ConnectionKey) -> None: # type: @pytest.mark.skipif( sys.implementation.name != "cpython", reason="CPython GC is required for the test" ) -def test_del_with_closed_loop( # type: ignore[misc] +def test_del_with_closed_loop( event_loop: asyncio.AbstractEventLoop, key: ConnectionKey, ) -> None: @@ -370,7 +370,7 @@ async def test_get(key: ConnectionKey) -> None: async def test_get_unconnected_proto() -> None: loop = asyncio.get_running_loop() conn = aiohttp.BaseConnector() - key = ConnectionKey("localhost", 80, False, False, None, None, None) + key = ConnectionKey("localhost", 80, False, False, None, None) try: assert await conn._get(key, []) is None @@ -392,7 +392,7 @@ async def test_get_unconnected_proto() -> None: async def test_get_unconnected_proto_ssl() -> None: loop = asyncio.get_running_loop() conn = aiohttp.BaseConnector() - key = ConnectionKey("localhost", 80, True, False, None, None, None) + key = ConnectionKey("localhost", 80, True, False, None, None) try: assert await conn._get(key, []) is None @@ -414,7 +414,7 @@ async def test_get_unconnected_proto_ssl() -> None: async def test_get_expired() -> None: loop = asyncio.get_running_loop() conn = aiohttp.BaseConnector() - key = ConnectionKey("localhost", 80, False, False, None, None, None) + key = ConnectionKey("localhost", 80, False, False, None, None) try: assert await conn._get(key, []) is None @@ -430,7 +430,7 @@ async def test_get_expired() -> None: async def test_get_expired_ssl() -> None: loop = asyncio.get_running_loop() conn = aiohttp.BaseConnector(enable_cleanup_closed=True) - key = ConnectionKey("localhost", 80, True, False, None, None, None) + key = ConnectionKey("localhost", 80, True, False, None, None) try: assert await conn._get(key, []) is None @@ -495,7 +495,7 @@ async def test_release(key: ConnectionKey) -> None: @pytest.mark.usefixtures("enable_cleanup_closed") -async def test_release_ssl_transport(ssl_key: ConnectionKey) -> None: # type: ignore[misc] +async def test_release_ssl_transport(ssl_key: ConnectionKey) -> None: conn = aiohttp.BaseConnector(enable_cleanup_closed=True) with mock.patch.object(conn, "_release_waiter", autospec=True, spec_set=True): proto = create_mocked_conn(asyncio.get_running_loop()) @@ -900,7 +900,7 @@ def get_extra_info(param: str) -> object: ("happy_eyeballs_delay"), [0.1, 0.25, None], ) -async def test_tcp_connector_happy_eyeballs( # type: ignore[misc] +async def test_tcp_connector_happy_eyeballs( happy_eyeballs_delay: float | None, make_client_request: _RequestMaker ) -> None: loop = asyncio.get_running_loop() @@ -989,7 +989,7 @@ async def create_connection( @pytest.mark.skipif(not HAS_IPV6, reason="IPv6 is not available") -async def test_tcp_connector_interleave(make_client_request: _RequestMaker) -> None: # type: ignore[misc] +async def test_tcp_connector_interleave(make_client_request: _RequestMaker) -> None: loop = asyncio.get_running_loop() conn = aiohttp.TCPConnector(interleave=2) @@ -1169,7 +1169,7 @@ async def create_connection( ("https://mocked.host"), ], ) -async def test_tcp_connector_multiple_hosts_one_timeout( # type: ignore[misc] +async def test_tcp_connector_multiple_hosts_one_timeout( request_url: str, make_client_request: _RequestMaker ) -> None: loop = asyncio.get_running_loop() @@ -1809,7 +1809,7 @@ async def test_connect_tracing(make_client_request: _RequestMaker) -> None: "on_connection_create_end", ], ) -async def test_exception_during_connetion_create_tracing( # type: ignore[misc] +async def test_exception_during_connetion_create_tracing( signal: str, make_client_request: _RequestMaker ) -> None: loop = asyncio.get_running_loop() @@ -2038,7 +2038,7 @@ async def test_cleanup(key: ConnectionKey) -> None: @pytest.mark.usefixtures("enable_cleanup_closed") -async def test_cleanup_close_ssl_transport( # type: ignore[misc] +async def test_cleanup_close_ssl_transport( ssl_key: ConnectionKey, ) -> None: proto = create_mocked_conn(asyncio.get_running_loop()) @@ -2208,7 +2208,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_pre_311() -> None: @pytest.mark.skipif( sys.version_info < (3, 11), reason="ssl_shutdown_timeout requires Python 3.11+" ) -async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( # type: ignore[misc] +async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( start_connection: mock.AsyncMock, make_client_request: _RequestMaker ) -> None: # Test that ssl_shutdown_timeout is passed to create_connection for SSL connections @@ -2270,7 +2270,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( @pytest.mark.skipif(sys.version_info >= (3, 11), reason="Test for Python < 3.11") -async def test_tcp_connector_ssl_shutdown_timeout_not_passed_pre_311( # type: ignore[misc] +async def test_tcp_connector_ssl_shutdown_timeout_not_passed_pre_311( start_connection: mock.AsyncMock, make_client_request: _RequestMaker ) -> None: # Test that ssl_shutdown_timeout is NOT passed to create_connection on Python < 3.11 @@ -2463,7 +2463,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_zero_not_passed( @pytest.mark.skipif( sys.version_info < (3, 11), reason="ssl_shutdown_timeout requires Python 3.11+" ) -async def test_tcp_connector_ssl_shutdown_timeout_nonzero_passed( # type: ignore[misc] +async def test_tcp_connector_ssl_shutdown_timeout_nonzero_passed( start_connection: mock.AsyncMock, make_client_request: _RequestMaker ) -> None: """Test that non-zero ssl_shutdown_timeout IS passed to create_connection on Python 3.11+.""" @@ -2509,7 +2509,7 @@ async def test_tcp_connector_close_abort_ssl_connections_in_conns() -> None: proto.transport = transport # Add the protocol to _conns - key = ConnectionKey("host", 443, True, True, None, None, None) + key = ConnectionKey("host", 443, True, True, None, None) conn._conns[key] = deque([(proto, asyncio.get_running_loop().time())]) # Close the connector @@ -3342,7 +3342,7 @@ async def test_connect_reuseconn_tracing( ("dont_use_proxy", False, False), ], ) -async def test_connect_reuse_proxy_headers( # type: ignore[misc] +async def test_connect_reuse_proxy_headers( make_client_request: _RequestMaker, test_case: str, wait_for_con: bool, @@ -3352,29 +3352,20 @@ async def test_connect_reuse_proxy_headers( # type: ignore[misc] proto = create_mocked_conn(loop) proto.is_connected.return_value = True - if test_case != "dont_use_proxy": - proxy = ( - URL("http://user:password@example.com") - if test_case == "use_proxy_with_embedded_auth" - else URL("http://example.com") - ) - proxy_headers = ( - CIMultiDict({hdrs.AUTHORIZATION: "Basic dXNlcjpwYXNzd29yZA=="}) - if test_case == "use_proxy_with_auth_headers" - else None + if test_case == "dont_use_proxy": + proxy = None + proxy_headers = None + elif test_case == "use_proxy_with_embedded_auth": + proxy = URL("http://user:password@example.com") + proxy_headers = None + elif test_case == "use_proxy_with_auth_headers": + proxy = URL("http://example.com") + proxy_headers = CIMultiDict( + {hdrs.PROXY_AUTHORIZATION: "Basic dXNlcjpwYXNzd29yZA=="} ) else: - proxy = None + proxy = URL("http://example.com") proxy_headers = None - key = ConnectionKey( - "localhost", - 80, - False, - True, - proxy, - None, - hash(tuple(proxy_headers.items())) if proxy_headers else None, - ) req = make_client_request( "GET", URL("http://localhost:80"), @@ -3383,6 +3374,9 @@ async def test_connect_reuse_proxy_headers( # type: ignore[misc] proxy=proxy, proxy_headers=proxy_headers, ) + # The request normalises proxy URL/credentials, so reuse its actual + # connection key for the pre-populated pool entry. + key = req.connection_key conn = aiohttp.BaseConnector(limit=1) @@ -3890,7 +3884,7 @@ async def handler(request: web.Request) -> web.Response: @pytest.mark.skipif(not hasattr(socket, "AF_UNIX"), reason="requires UNIX sockets") -async def test_unix_connector_not_found( # type: ignore[misc] +async def test_unix_connector_not_found( make_client_request: _RequestMaker, ) -> None: connector = aiohttp.UnixConnector("/" + uuid.uuid4().hex) @@ -3903,7 +3897,7 @@ async def test_unix_connector_not_found( # type: ignore[misc] @pytest.mark.skipif(not hasattr(socket, "AF_UNIX"), reason="requires UNIX sockets") -async def test_unix_connector_permission( # type: ignore[misc] +async def test_unix_connector_permission( make_client_request: _RequestMaker, ) -> None: loop = asyncio.get_running_loop() @@ -3929,7 +3923,7 @@ async def test_named_pipe_connector_wrong_loop(pipe_name: str) -> None: platform.system() != "Windows", reason="Proactor Event loop present only in Windows" ) @pytest.mark.asyncio(loop_factories=("proactor",)) -async def test_named_pipe_connector_not_found( # type: ignore[misc] +async def test_named_pipe_connector_not_found( pipe_name: str, make_client_request: _RequestMaker, ) -> None: @@ -3945,7 +3939,7 @@ async def test_named_pipe_connector_not_found( # type: ignore[misc] platform.system() != "Windows", reason="Proactor Event loop present only in Windows" ) @pytest.mark.asyncio(loop_factories=("proactor",)) -async def test_named_pipe_connector_permission( # type: ignore[misc] +async def test_named_pipe_connector_permission( pipe_name: str, make_client_request: _RequestMaker, ) -> None: @@ -4634,7 +4628,7 @@ async def test_available_connections_with_limit_per_host( @pytest.mark.parametrize("limit_per_host", [0, 10]) -async def test_available_connections_without_limit_per_host( # type: ignore[misc] +async def test_available_connections_without_limit_per_host( key: ConnectionKey, other_host_key2: ConnectionKey, limit_per_host: int ) -> None: """Verify expected values based on active connections with higher host limit.""" diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 500dd89a849..144c3677c9e 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,9 +1,7 @@ import asyncio -import base64 import datetime import gc import sys -import warnings import weakref from collections.abc import Iterator from math import ceil, modf @@ -125,162 +123,56 @@ def test_guess_filename_with_default() -> None: assert helpers.guess_filename(None, "no-throw") == "no-throw" -# ------------------- BasicAuth ----------------------------------- +# ------------------- encode_basic_auth ----------------------------------- -def test_basic_auth1() -> None: - # missing password here - with pytest.raises(ValueError): - helpers.BasicAuth(None) # type: ignore[arg-type] - - -def test_basic_auth2() -> None: - with pytest.raises(ValueError): - helpers.BasicAuth("nkim", None) # type: ignore[arg-type] - - -def test_basic_with_auth_colon_in_login() -> None: - with pytest.raises(ValueError): - helpers.BasicAuth("nkim:1", "pwd") +def test_encode_basic_auth() -> None: + assert helpers.encode_basic_auth("nkim", "pwd") == "Basic bmtpbTpwd2Q=" -def test_basic_auth3() -> None: - with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): - auth = helpers.BasicAuth("nkim") - assert auth.login == "nkim" - assert auth.password == "" +def test_encode_basic_auth_default_password() -> None: + assert helpers.encode_basic_auth("nkim") == "Basic bmtpbTo=" -def test_basic_auth4() -> None: - with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"): - auth = helpers.BasicAuth("nkim", "pwd") - assert auth.login == "nkim" - assert auth.password == "pwd" - assert auth.encode() == "Basic bmtpbTpwd2Q=" +def test_encode_basic_auth_blank_login_and_password() -> None: + assert helpers.encode_basic_auth("") == "Basic Og==" -def test_basic_auth_deprecated() -> None: - with pytest.warns( - DeprecationWarning, - match=( - "BasicAuth is deprecated and will be removed in aiohttp 4.0; " - "use aiohttp.encode_basic_auth" - ), - ): - helpers.BasicAuth("user", "pass") +def test_encode_basic_auth_utf8() -> None: + assert helpers.encode_basic_auth("usér", "pàss") == "Basic dXPDqXI6cMOgc3M=" -def test_encode_basic_auth() -> None: - assert helpers.encode_basic_auth("nkim", "pwd") == "Basic bmtpbTpwd2Q=" - assert helpers.encode_basic_auth("") == "Basic Og==" +def test_encode_basic_auth_latin1() -> None: assert ( - helpers.encode_basic_auth("usér", "pàss", encoding="utf-8") - == "Basic dXPDqXI6cMOgc3M=" + helpers.encode_basic_auth("nkim", "café", encoding="latin1") + == "Basic bmtpbTpjYWbp" ) def test_encode_basic_auth_rejects_colon_in_login() -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r'":" is not allowed in login.*RFC 7617'): helpers.encode_basic_auth("user:1", "pwd") -def test_basic_auth_no_warn_helpers_silent() -> None: - """Internal aiohttp paths must not raise BasicAuth's deprecation warning.""" - with warnings.catch_warnings(): - warnings.simplefilter("error", DeprecationWarning) - url = URL("http://user:pass@example.com/") - helpers.strip_auth_from_url(url) - helpers.BasicAuth.decode("Basic dXNlcjpwYXNz") - helpers.BasicAuth.from_url(url) - helpers._basic_auth_no_warn("user", "pass") - - -@pytest.mark.parametrize( - "header", - ( - "Basic bmtpbTpwd2Q=", - "basic bmtpbTpwd2Q=", - ), -) -def test_basic_auth_decode(header: str) -> None: - auth = helpers.BasicAuth.decode(header) - assert auth.login == "nkim" - assert auth.password == "pwd" - - -def test_basic_auth_invalid() -> None: - with pytest.raises(ValueError): - helpers.BasicAuth.decode("bmtpbTpwd2Q=") - - -def test_basic_auth_decode_not_basic() -> None: - with pytest.raises(ValueError): - helpers.BasicAuth.decode("Complex bmtpbTpwd2Q=") - - -def test_basic_auth_decode_bad_base64() -> None: - with pytest.raises(ValueError): - helpers.BasicAuth.decode("Basic bmtpbTpwd2Q") - - -@pytest.mark.parametrize("header", ("Basic ???", "Basic ")) -def test_basic_auth_decode_illegal_chars_base64(header: str) -> None: - with pytest.raises(ValueError, match="Invalid base64 encoding."): - helpers.BasicAuth.decode(header) - - -def test_basic_auth_decode_invalid_credentials() -> None: - with pytest.raises(ValueError, match="Invalid credentials."): - header = "Basic {}".format(base64.b64encode(b"username").decode()) - helpers.BasicAuth.decode(header) - - -@pytest.mark.parametrize( - "credentials, expected_auth", - ( - (":", helpers._basic_auth_no_warn("", "", "latin1")), - ("username:", helpers._basic_auth_no_warn("username", "", "latin1")), - (":password", helpers._basic_auth_no_warn("", "password", "latin1")), - ( - "username:password", - helpers._basic_auth_no_warn("username", "password", "latin1"), - ), - ), -) -def test_basic_auth_decode_blank_username( # type: ignore[misc] - credentials: str, expected_auth: helpers.BasicAuth -) -> None: - header = f"Basic {base64.b64encode(credentials.encode()).decode()}" - assert helpers.BasicAuth.decode(header) == expected_auth - - -def test_basic_auth_from_url() -> None: - url = URL("http://user:pass@example.com") - auth = helpers.BasicAuth.from_url(url) - assert auth is not None - assert auth.login == "user" - assert auth.password == "pass" +def test_strip_auth_from_url() -> None: + url, auth = helpers.strip_auth_from_url(URL("http://user:pass@example.com/")) + assert url == URL("http://example.com/") + assert auth == helpers.encode_basic_auth("user", "pass") -def test_basic_auth_no_user_from_url() -> None: - url = URL("http://:pass@example.com") - auth = helpers.BasicAuth.from_url(url) - assert auth is not None - assert auth.login == "" - assert auth.password == "pass" +def test_strip_auth_from_url_no_user() -> None: + url, auth = helpers.strip_auth_from_url(URL("http://:pass@example.com/")) + assert url == URL("http://example.com/") + assert auth == helpers.encode_basic_auth("", "pass") -def test_basic_auth_no_auth_from_url() -> None: - url = URL("http://example.com") - auth = helpers.BasicAuth.from_url(url) +def test_strip_auth_from_url_no_auth() -> None: + url = URL("http://example.com/") + stripped, auth = helpers.strip_auth_from_url(url) + assert stripped is url assert auth is None -def test_basic_auth_from_not_url() -> None: - with pytest.raises(TypeError): - helpers.BasicAuth.from_url("http://user:pass@example.com") # type: ignore[arg-type] - - # ----------------------------------- is_ip_address() ---------------------- @@ -678,11 +570,7 @@ def test_proxies_from_env_http_with_auth(url_input: str, expected_scheme: str) - ret = helpers.proxies_from_env() assert ret.keys() == {expected_scheme} assert ret[expected_scheme].proxy == url.with_user(None) - proxy_auth = ret[expected_scheme].proxy_auth - assert proxy_auth is not None - assert proxy_auth.login == "user" - assert proxy_auth.password == "pass" - assert proxy_auth.encoding == "latin1" + assert ret[expected_scheme].proxy_auth == helpers.encode_basic_auth("user", "pass") # --------------------- get_env_proxy_for_url ------------------------------ @@ -1138,31 +1026,29 @@ def test_netrc_from_home_does_not_raise_if_access_denied( @pytest.mark.parametrize( - ["netrc_contents", "expected_auth"], + ["netrc_contents", "expected_header"], [ ( "machine example.com login username password pass\n", - helpers._basic_auth_no_warn("username", "pass", "latin1"), + helpers.encode_basic_auth("username", "pass"), ), ( "machine example.com account username password pass\n", - helpers._basic_auth_no_warn("username", "pass", "latin1"), + helpers.encode_basic_auth("username", "pass"), ), ( "machine example.com password pass\n", - helpers._basic_auth_no_warn("", "pass", "latin1"), + helpers.encode_basic_auth("", "pass"), ), ], indirect=("netrc_contents",), ) @pytest.mark.usefixtures("netrc_contents") -def test_basicauth_present_in_netrc( # type: ignore[misc] - expected_auth: helpers.BasicAuth, -) -> None: - """Test that netrc file contents are properly parsed into BasicAuth tuples""" +def test_auth_header_from_netrc(expected_header: str) -> None: + """Test that netrc file contents are properly parsed into a header value.""" netrc_obj = helpers.netrc_from_env() - assert expected_auth == helpers.basicauth_from_netrc(netrc_obj, "example.com") + assert expected_header == helpers._auth_header_from_netrc(netrc_obj, "example.com") @pytest.mark.parametrize( @@ -1173,14 +1059,14 @@ def test_basicauth_present_in_netrc( # type: ignore[misc] indirect=("netrc_contents",), ) @pytest.mark.usefixtures("netrc_contents") -def test_read_basicauth_from_empty_netrc() -> None: +def test_read_auth_header_from_empty_netrc() -> None: """Test that an error is raised if netrc doesn't have an entry for our host""" netrc_obj = helpers.netrc_from_env() with pytest.raises( LookupError, match="No entry for example.com found in the `.netrc` file." ): - helpers.basicauth_from_netrc(netrc_obj, "example.com") + helpers._auth_header_from_netrc(netrc_obj, "example.com") def test_method_must_be_empty_body() -> None: diff --git a/tests/test_proxy.py b/tests/test_proxy.py index c96bbcf1c3a..b4edcc2790a 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -10,6 +10,7 @@ from yarl import URL import aiohttp +from aiohttp import hdrs from aiohttp.client_reqrep import ( ClientRequest, ClientRequestArgs, @@ -81,7 +82,6 @@ async def test_connect( # type: ignore[misc] ClientRequestMock.assert_called_with( "GET", URL("http://proxy.example.com"), - auth=None, headers={"Host": "www.python.org"}, loop=event_loop, ssl=True, @@ -143,7 +143,6 @@ async def test_proxy_headers( # type: ignore[misc] ClientRequestMock.assert_called_with( "GET", URL("http://proxy.example.com"), - auth=None, headers={"Host": "www.python.org", "Foo": "Bar"}, loop=event_loop, ssl=True, @@ -153,26 +152,6 @@ async def test_proxy_headers( # type: ignore[misc] await connector.close() -@mock.patch( - "aiohttp.connector.aiohappyeyeballs.start_connection", - autospec=True, - spec_set=True, -) -async def test_proxy_auth( # type: ignore[misc] - start_connection: mock.Mock, - make_client_request: _RequestMaker, -) -> None: - msg = r"proxy_auth must be None or BasicAuth\(\) tuple" - with pytest.raises(ValueError, match=msg): - make_client_request( - "GET", - URL("http://python.org"), - proxy=URL("http://proxy.example.com"), - proxy_auth=("user", "pass"), # type: ignore[arg-type] - loop=mock.Mock(), - ) - - @mock.patch( "aiohttp.connector.aiohappyeyeballs.start_connection", autospec=True, @@ -254,7 +233,6 @@ async def test_proxy_server_hostname_default( # type: ignore[misc] proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=None, loop=event_loop, ssl=True, headers=CIMultiDict({}), @@ -338,7 +316,6 @@ async def test_proxy_server_hostname_override( # type: ignore[misc] proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=None, loop=event_loop, ssl=True, headers=CIMultiDict({}), @@ -425,7 +402,6 @@ async def test_https_connect_fingerprint_mismatch( # type: ignore[misc] proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=None, loop=event_loop, ssl=True, headers=CIMultiDict({}), @@ -535,7 +511,6 @@ async def test_https_connect( # type: ignore[misc] proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=None, loop=event_loop, ssl=True, headers=CIMultiDict({}), @@ -618,7 +593,6 @@ async def test_https_connect_certificate_error( # type: ignore[misc] proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=None, loop=event_loop, ssl=True, headers=CIMultiDict({}), @@ -697,7 +671,6 @@ async def test_https_connect_ssl_error( # type: ignore[misc] proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=None, loop=event_loop, ssl=True, headers=CIMultiDict({}), @@ -776,7 +749,6 @@ async def test_https_connect_http_proxy_error( # type: ignore[misc] proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=None, loop=event_loop, ssl=True, headers=CIMultiDict({}), @@ -855,7 +827,6 @@ async def test_https_connect_resp_start_error( # type: ignore[misc] proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=None, loop=event_loop, ssl=True, headers=CIMultiDict({}), @@ -957,32 +928,6 @@ async def test_request_port( # type: ignore[misc] await connector.close() -async def test_proxy_auth_property( - event_loop: asyncio.AbstractEventLoop, make_client_request: _RequestMaker -) -> None: - req = make_client_request( - "GET", - URL("http://localhost:1234/path"), - proxy=URL("http://proxy.example.com"), - proxy_auth=aiohttp.helpers._basic_auth_no_warn("user", "pass"), - loop=event_loop, - ) - assert ("user", "pass", "latin1") == req.proxy_auth - - -async def test_proxy_auth_property_default( - event_loop: asyncio.AbstractEventLoop, - make_client_request: _RequestMaker, -) -> None: - req = make_client_request( - "GET", - URL("http://localhost:1234/path"), - proxy=URL("http://proxy.example.com"), - loop=event_loop, - ) - assert req.proxy_auth is None - - @mock.patch("aiohttp.connector.ClientRequestBase") @mock.patch( "aiohttp.connector.aiohappyeyeballs.start_connection", @@ -998,7 +943,6 @@ async def test_https_connect_pass_ssl_context( # type: ignore[misc] proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=None, loop=event_loop, ssl=True, headers=CIMultiDict({}), @@ -1087,13 +1031,13 @@ async def test_https_auth( # type: ignore[misc] make_client_request: _RequestMaker, ) -> None: event_loop = asyncio.get_running_loop() + proxy_auth_header = aiohttp.encode_basic_auth("user", "pass") proxy_req = ClientRequestBase( "GET", URL("http://proxy.example.com"), - auth=aiohttp.helpers._basic_auth_no_warn("user", "pass"), loop=event_loop, ssl=True, - headers=CIMultiDict({}), + headers=CIMultiDict({hdrs.PROXY_AUTHORIZATION: proxy_auth_header}), ) ClientRequestMock.return_value = proxy_req @@ -1139,8 +1083,8 @@ async def test_https_auth( # type: ignore[misc] autospec=True, return_value=mock.Mock(), ): - assert "AUTHORIZATION" in proxy_req.headers - assert "PROXY-AUTHORIZATION" not in proxy_req.headers + assert "AUTHORIZATION" not in proxy_req.headers + assert "PROXY-AUTHORIZATION" in proxy_req.headers req = make_client_request( "GET", diff --git a/tests/test_proxy_functional.py b/tests/test_proxy_functional.py index 2df3915f617..0cb6eaac712 100644 --- a/tests/test_proxy_functional.py +++ b/tests/test_proxy_functional.py @@ -25,16 +25,6 @@ ASYNCIO_SUPPORTS_TLS_IN_TLS = sys.version_info >= (3, 11) -pytestmark = [ - pytest.mark.filterwarnings(r"ignore:BasicAuth is deprecated:DeprecationWarning"), - pytest.mark.filterwarnings( - r"ignore:The 'auth' parameter is deprecated:DeprecationWarning" - ), - pytest.mark.filterwarnings( - r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning" - ), -] - class _ResponseArgs(TypedDict): status: int @@ -405,18 +395,27 @@ async def test_proxy_http_auth( assert "Authorization" not in proxy.request.headers assert "Proxy-Authorization" not in proxy.request.headers - auth = aiohttp.BasicAuth("user", "pass") - await get_request(url=url, auth=auth, proxy=proxy.url) + auth_header = aiohttp.encode_basic_auth("user", "pass") + await get_request(url=url, headers={"Authorization": auth_header}, proxy=proxy.url) assert "Authorization" in proxy.request.headers assert "Proxy-Authorization" not in proxy.request.headers - await get_request(url=url, proxy_auth=auth, proxy=proxy.url) + await get_request( + url=url, + proxy_headers={"Proxy-Authorization": auth_header}, + proxy=proxy.url, + ) assert "Authorization" not in proxy.request.headers assert "Proxy-Authorization" in proxy.request.headers - await get_request(url=url, auth=auth, proxy_auth=auth, proxy=proxy.url) + await get_request( + url=url, + headers={"Authorization": auth_header}, + proxy_headers={"Proxy-Authorization": auth_header}, + proxy=proxy.url, + ) assert "Authorization" in proxy.request.headers assert "Proxy-Authorization" in proxy.request.headers @@ -426,10 +425,10 @@ async def test_proxy_http_auth_utf8( proxy_test_server: Callable[[], Awaitable[mock.Mock]], ) -> None: url = "http://aiohttp.io/path" - auth = aiohttp.BasicAuth("юзер", "пасс", "utf-8") + auth_header = aiohttp.encode_basic_auth("юзер", "пасс") proxy = await proxy_test_server() - await get_request(url=url, auth=auth, proxy=proxy.url) + await get_request(url=url, headers={"Authorization": auth_header}, proxy=proxy.url) assert "Authorization" in proxy.request.headers assert "Proxy-Authorization" not in proxy.request.headers @@ -631,7 +630,7 @@ async def test_proxy_https_auth( proxy_test_server: Callable[[], Awaitable[mock.Mock]], ) -> None: url = "https://secure.aiohttp.io/path" - auth = aiohttp.BasicAuth("user", "pass") + auth_header = aiohttp.encode_basic_auth("user", "pass") proxy = await proxy_test_server() await get_request(url=url, proxy=proxy.url) @@ -643,7 +642,7 @@ async def test_proxy_https_auth( assert "Proxy-Authorization" not in proxy.request.headers proxy = await proxy_test_server() - await get_request(url=url, auth=auth, proxy=proxy.url) + await get_request(url=url, headers={"Authorization": auth_header}, proxy=proxy.url) connect = proxy.requests_list[0] assert "Authorization" not in connect.headers @@ -652,7 +651,11 @@ async def test_proxy_https_auth( assert "Proxy-Authorization" not in proxy.request.headers proxy = await proxy_test_server() - await get_request(url=url, proxy_auth=auth, proxy=proxy.url) + await get_request( + url=url, + proxy_headers={"Proxy-Authorization": auth_header}, + proxy=proxy.url, + ) connect = proxy.requests_list[0] assert "Authorization" not in connect.headers @@ -661,7 +664,12 @@ async def test_proxy_https_auth( assert "Proxy-Authorization" not in proxy.request.headers proxy = await proxy_test_server() - await get_request(url=url, auth=auth, proxy_auth=auth, proxy=proxy.url) + await get_request( + url=url, + headers={"Authorization": auth_header}, + proxy_headers={"Proxy-Authorization": auth_header}, + proxy=proxy.url, + ) connect = proxy.requests_list[0] assert "Authorization" not in connect.headers @@ -814,14 +822,10 @@ async def test_proxy_from_env_http_with_auth( ) -> None: url = "http://aiohttp.io/path" proxy = await proxy_test_server() - auth = aiohttp.BasicAuth("user", "pass") + expected_header = aiohttp.encode_basic_auth("user", "pass") mocker.patch.dict( os.environ, - { - "http_proxy": str( - proxy.url.with_user(auth.login).with_password(auth.password) - ) - }, + {"http_proxy": str(proxy.url.with_user("user").with_password("pass"))}, ) await get_request(url=url, trust_env=True) @@ -830,7 +834,7 @@ async def test_proxy_from_env_http_with_auth( assert proxy.request.method == "GET" assert proxy.request.host == "aiohttp.io" assert proxy.request.path_qs == "/path" - assert proxy.request.headers["Proxy-Authorization"] == auth.encode() + assert proxy.request.headers["Proxy-Authorization"] == expected_header async def test_proxy_from_env_http_with_auth_from_netrc( @@ -840,11 +844,9 @@ async def test_proxy_from_env_http_with_auth_from_netrc( ) -> None: url = "http://aiohttp.io/path" proxy = await proxy_test_server() - auth = aiohttp.BasicAuth("user", "pass") + expected_header = aiohttp.encode_basic_auth("user", "pass") netrc_file = tmp_path / "test_netrc" - netrc_file_data = f"machine 127.0.0.1 login {auth.login} password {auth.password}" - with netrc_file.open("w") as f: - f.write(netrc_file_data) + netrc_file.write_text("machine 127.0.0.1 login user password pass") mocker.patch.dict( os.environ, {"http_proxy": str(proxy.url), "NETRC": str(netrc_file)} ) @@ -855,7 +857,7 @@ async def test_proxy_from_env_http_with_auth_from_netrc( assert proxy.request.method == "GET" assert proxy.request.host == "aiohttp.io" assert proxy.request.path_qs == "/path" - assert proxy.request.headers["Proxy-Authorization"] == auth.encode() + assert proxy.request.headers["Proxy-Authorization"] == expected_header async def test_proxy_from_env_http_without_auth_from_netrc( @@ -865,11 +867,8 @@ async def test_proxy_from_env_http_without_auth_from_netrc( ) -> None: url = "http://aiohttp.io/path" proxy = await proxy_test_server() - auth = aiohttp.BasicAuth("user", "pass") netrc_file = tmp_path / "test_netrc" - netrc_file_data = f"machine 127.0.0.2 login {auth.login} password {auth.password}" - with netrc_file.open("w") as f: - f.write(netrc_file_data) + netrc_file.write_text("machine 127.0.0.2 login user password pass") mocker.patch.dict( os.environ, {"http_proxy": str(proxy.url), "NETRC": str(netrc_file)} ) @@ -890,12 +889,8 @@ async def test_proxy_from_env_http_without_auth_from_wrong_netrc( ) -> None: url = "http://aiohttp.io/path" proxy = await proxy_test_server() - auth = aiohttp.BasicAuth("user", "pass") netrc_file = tmp_path / "test_netrc" - invalid_data = f"machine 127.0.0.1 {auth.login} pass {auth.password}" - with netrc_file.open("w") as f: - f.write(invalid_data) - + netrc_file.write_text("machine 127.0.0.1 user pass pass") mocker.patch.dict( os.environ, {"http_proxy": str(proxy.url), "NETRC": str(netrc_file)} ) @@ -933,14 +928,10 @@ async def test_proxy_from_env_https_with_auth( ) -> None: url = "https://aiohttp.io/path" proxy = await proxy_test_server() - auth = aiohttp.BasicAuth("user", "pass") + expected_header = aiohttp.encode_basic_auth("user", "pass") mocker.patch.dict( os.environ, - { - "https_proxy": str( - proxy.url.with_user(auth.login).with_password(auth.password) - ) - }, + {"https_proxy": str(proxy.url.with_user("user").with_password("pass"))}, ) await get_request(url=url, trust_env=True) @@ -956,20 +947,7 @@ async def test_proxy_from_env_https_with_auth( assert r2.method == "CONNECT" assert r2.host == "aiohttp.io" assert r2.path_qs == "/path" - assert r2.headers["Proxy-Authorization"] == auth.encode() - - -async def test_proxy_auth() -> None: - async with aiohttp.ClientSession() as session: - with pytest.raises( - ValueError, match=r"proxy_auth must be None or BasicAuth\(\) tuple" - ): - async with session.get( - "http://python.org", - proxy="http://proxy.example.com", - proxy_auth=("user", "pass"), # type: ignore[arg-type] - ): - pass + assert r2.headers["Proxy-Authorization"] == expected_header async def test_https_proxy_connect_tunnel_session_close_no_hang(