From 0a725d1dab57ee453f2475b72910b6b5722f8e9f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 19:47:54 +0000 Subject: [PATCH 1/5] Bump ast-serialize from 0.4.0 to 0.5.0 (#12631) Bumps [ast-serialize](https://github.com/mypyc/ast_serialize) from 0.4.0 to 0.5.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ast-serialize&package-manager=pip&previous-version=0.4.0&new-version=0.5.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/constraints.txt | 5 ++++- requirements/dev.txt | 5 ++++- requirements/lint.txt | 2 +- requirements/test-common.txt | 3 ++- requirements/test-ft.txt | 3 ++- requirements/test.txt | 3 ++- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 10c1d61adf3..be641ee0b18 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -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 @@ -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..4c22f34133b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -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 @@ -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/lint.txt b/requirements/lint.txt index 4c70287b346..635f66d8a76 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -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 diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 1faa93e7c1f..99824dd2b84 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 @@ -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..062d4fbcff1 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -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 @@ -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..0fccae92718 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -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 @@ -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 From 082ff8c78ca13997b1f1d305dd535efb82c103e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 20:14:43 +0000 Subject: [PATCH 2/5] Bump click from 8.3.3 to 8.4.0 (#12632) Bumps [click](https://github.com/pallets/click) from 8.3.3 to 8.4.0.
Release notes

Sourced from click's releases.

8.4.0

This is the Click 8.4.0 feature release. A feature release may include new features, remove previously deprecated code, add new deprecation, or introduce potentially breaking changes.

We encourage everyone to upgrade. You can read more about our Version Support Policy on our website.

PyPI: https://pypi.org/project/click/8.4.0/ Changes: https://click.palletsprojects.com/page/changes/#version-8-4-0 Milestone https://github.com/pallets/click/milestone/30

  • ParamType typing improvements. #3371

    • :class:ParamType is now a generic abstract base class, parameterized by its converted value type.
    • :meth:~ParamType.convert return types are narrowed on all concrete types (str for :class:STRING, int for :class:INT, etc.).
    • :meth:~ParamType.to_info_dict returns specific :class:~typing.TypedDict subclasses instead of dict[str, Any].
    • :class:CompositeParamType and the number-range base are now generic with abstract methods.
  • Refactor convert_type to extract type inference into a private _guess_type helper, and add :func:typing.overload signatures. #3372

  • Parameter typing improvements. #2805

    • :class:Parameter is now an abstract base class, making explicit that it cannot be instantiated directly.
    • :attr:Parameter.name is now str instead of str | None. When expose_value=False, the name is set to "" instead of None.
    • The ctx parameter of :meth:Parameter.get_error_hint is now typed as Context | None, matching the runtime behavior.
  • Split string values from default_map for parameters with nargs > 1 or :class:Tuple type, matching environment variable behavior. #2745 #3364

  • Auto-detect type=UNPROCESSED for flag_value of non-basic types (not str, int, float, or bool), so programmer-provided Python objects like classes and enum members are passed through unchanged instead of being stringified. Previously type=click.UNPROCESSED had to be set explicitly. #2012 #3363

  • The error hint now uses Command.get_help_option_names to pick non-shadowed help option names, so Try '... -h' no longer points to a subcommand option that shadows -h. All surviving names are shown (-h/--help). #2790 #3208

  • Fix readline functionality on non-Windows platforms. Prompt text is now passed directly to readline instead of being printed separately, allowing proper backspace, line editing, and line wrapping behavior. #2968

... (truncated)

Changelog

Sourced from click's changelog.

Version 8.4.0

Released 2026-05-17

  • :class:ParamType typing improvements. :pr:3371

    • :class:ParamType is now a generic abstract base class, parameterized by its converted value type.
    • :meth:~ParamType.convert return types are narrowed on all concrete types (str for :class:STRING, int for :class:INT, etc.).
    • :meth:~ParamType.to_info_dict returns specific :class:~typing.TypedDict subclasses instead of dict[str, Any].
    • :class:CompositeParamType and the number-range base are now generic with abstract methods.
  • Refactor convert_type to extract type inference into a private _guess_type helper, and add :func:typing.overload signatures. :pr:3372

  • :class:Parameter typing improvements. :pr:2805

    • :class:Parameter is now an abstract base class, making explicit that it cannot be instantiated directly.
    • :attr:Parameter.name is now str instead of str | None. When expose_value=False, the name is set to "" instead of None.
    • The ctx parameter of :meth:Parameter.get_error_hint is now typed as Context | None, matching the runtime behavior.
  • Split string values from default_map for parameters with nargs > 1 or :class:Tuple type, matching environment variable behavior. :issue:2745 :pr:3364

  • Auto-detect type=UNPROCESSED for flag_value of non-basic types (not str, int, float, or bool), so programmer-provided Python objects like classes and enum members are passed through unchanged instead of being stringified. Previously type=click.UNPROCESSED had to be set explicitly. :issue:2012 :pr:3363

  • The error hint now uses :meth:Command.get_help_option_names to pick non-shadowed help option names, so Try '... -h' no longer points to a subcommand option that shadows -h. All surviving names are shown (-h/--help). :issue:2790 :pr:3208

  • Fix readline functionality on non-Windows platforms. Prompt text is now passed directly to readline instead of being printed separately, allowing proper backspace, line editing, and line wrapping behavior. :issue:2968 :pr:2969

  • Use :func:os.startfile on Windows to open URLs in :func:open_url, replacing the start built-in which cannot be invoked without shell=True. :issue:3164 :pr:3186

  • Fix Fish shell completion errors when option help text contains newlines. :issue:3043 :pr:3126

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=click&package-manager=pip&previous-version=8.3.3&new-version=8.4.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/doc-spelling.txt | 2 +- requirements/doc.txt | 2 +- requirements/lint.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index be641ee0b18..f483f5fa571 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -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 diff --git a/requirements/dev.txt b/requirements/dev.txt index 4c22f34133b..d18265ca1c7 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -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 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 635f66d8a76..e23a86e68db 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -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/test-common.txt b/requirements/test-common.txt index 99824dd2b84..f21137a4ff5 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -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 diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 062d4fbcff1..c5a444c4698 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -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 diff --git a/requirements/test.txt b/requirements/test.txt index 0fccae92718..862b1b1bee8 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -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 From aff0eaa63eff06f0b1544a81254c3b23b7a4dc05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 20:23:03 +0000 Subject: [PATCH 3/5] Bump aiodns from 4.0.0 to 4.0.3 (#12633) Bumps [aiodns](https://github.com/aio-libs/aiodns) from 4.0.0 to 4.0.3.
Changelog

Sourced from aiodns's changelog.

4.0.3

  • Restore license metadata that was dropped during the pyproject.toml migration in #244, so packaging tools again detect aiodns as MIT-licensed (#250).

4.0.2

  • Re-release of 4.0.1; the 4.0.1 wheel build failed because the release workflow still invoked python setup.py after #244 removed setup.py, so 4.0.1 never reached PyPI. The release workflow now uses python -m build (#248).

4.0.1

  • Fix Future exception was never retrieved when pycares raises AresError synchronously, e.g. for malformed hostnames (#245, fixes #231)
  • Modernized package setup using pyproject.toml instead of setup.py (#244)
  • Updated dependencies
    • Bumped mypy from 1.19.1 to 2.1.0 (#236, #239, #241, #242)
    • Bumped pytest from 9.0.2 to 9.0.3 (#237)
    • Bumped pytest-cov from 7.0.0 to 7.1.0 (#232)
    • Bumped dependabot/fetch-metadata from 2.4.0 to 3.1.0 (#227, #234, #240)
    • Bumped actions/download-artifact from 7.0.0 to 8.0.1 (#228, #235)
    • Bumped actions/upload-artifact from 6 to 7 (#229)
    • Bumped codecov/codecov-action from 5 to 6 (#233)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aiodns&package-manager=pip&previous-version=4.0.0&new-version=4.0.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/base-ft.txt | 2 +- requirements/base.txt | 2 +- requirements/constraints.txt | 2 +- requirements/dev.txt | 2 +- requirements/lint.txt | 2 +- requirements/runtime-deps.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) 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 f483f5fa571..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 diff --git a/requirements/dev.txt b/requirements/dev.txt index d18265ca1c7..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 diff --git a/requirements/lint.txt b/requirements/lint.txt index e23a86e68db..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 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-ft.txt b/requirements/test-ft.txt index c5a444c4698..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 diff --git a/requirements/test.txt b/requirements/test.txt index 862b1b1bee8..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 From 247b3238ca5e8d6c03c7f15e341dbace4aca254f Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 18 May 2026 22:36:58 +0100 Subject: [PATCH 4/5] Remove BasicAuth (#12554) --- CHANGES.rst | 8 +- CHANGES/12499.deprecation.rst | 2 +- CHANGES/12499.feature.rst | 2 +- CHANGES/12501.breaking.rst | 3 + aiohttp/__init__.py | 3 +- aiohttp/client.py | 155 ++++--------------- aiohttp/client_reqrep.py | 62 +++----- aiohttp/connector.py | 18 +-- aiohttp/helpers.py | 104 ++----------- docs/client_advanced.rst | 12 -- docs/client_reference.rst | 96 +----------- docs/spelling_wordlist.txt | 1 - examples/client_auth.py | 5 +- tests/conftest.py | 2 - tests/test_benchmarks_client_request.py | 4 - tests/test_client_exceptions.py | 2 - tests/test_client_functional.py | 197 ++++-------------------- tests/test_client_request.py | 60 +++----- tests/test_client_session.py | 56 +------ tests/test_connector.py | 86 +++++------ tests/test_helpers.py | 184 +++++----------------- tests/test_proxy.py | 66 +------- tests/test_proxy_functional.py | 100 +++++------- 23 files changed, 262 insertions(+), 966 deletions(-) create mode 100644 CHANGES/12501.breaking.rst 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/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/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( From 66cc7b68aad1ca084d210a466d4970de9619f10e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 May 2026 15:31:07 -0700 Subject: [PATCH 5/5] Install Python via astral-sh/setup-uv in test/autobahn jobs (#12629) --- .github/workflows/ci-cd.yml | 52 +++++++++++++------------------------ CHANGES/12629.contrib.rst | 7 +++++ 2 files changed, 25 insertions(+), 34 deletions(-) create mode 100644 CHANGES/12629.contrib.rst 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/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`.