Compare commits

..

1 Commits

Author SHA1 Message Date
Nate Prewitt
35ba878a72 Move class types to init where possible
Some checks failed
Lint code / lint (push) Has been cancelled
Tests / build (macOS-latest, 3.10) (push) Has been cancelled
Tests / build (macOS-latest, 3.11) (push) Has been cancelled
Tests / build (macOS-latest, 3.12) (push) Has been cancelled
Tests / build (macOS-latest, 3.13) (push) Has been cancelled
Tests / build (macOS-latest, 3.14) (push) Has been cancelled
Tests / build (macOS-latest, 3.15-dev) (push) Has been cancelled
Tests / build (macOS-latest, pypy-3.11) (push) Has been cancelled
Tests / build (ubuntu-22.04, 3.10) (push) Has been cancelled
Tests / build (ubuntu-22.04, 3.11) (push) Has been cancelled
Tests / build (ubuntu-22.04, 3.12) (push) Has been cancelled
Tests / build (ubuntu-22.04, 3.13) (push) Has been cancelled
Tests / build (ubuntu-22.04, 3.14) (push) Has been cancelled
Tests / build (ubuntu-22.04, 3.15-dev) (push) Has been cancelled
Tests / build (ubuntu-22.04, pypy-3.11) (push) Has been cancelled
Tests / build (windows-latest, 3.10) (push) Has been cancelled
Tests / build (windows-latest, 3.11) (push) Has been cancelled
Tests / build (windows-latest, 3.12) (push) Has been cancelled
Tests / build (windows-latest, 3.13) (push) Has been cancelled
Tests / build (windows-latest, 3.14) (push) Has been cancelled
Tests / build (windows-latest, 3.15-dev) (push) Has been cancelled
Tests / No Character Detection (push) Has been cancelled
Tests / urllib3 1.x (push) Has been cancelled
Type Check / typecheck (3.10) (push) Has been cancelled
Type Check / typecheck (3.14) (push) Has been cancelled
2026-05-07 18:14:26 -06:00
36 changed files with 174 additions and 487 deletions

81
.github/AI_POLICY.md vendored
View File

@@ -1,81 +0,0 @@
# Generative AI / LLM Policy
We appreciate that we can't realistically police how you author your pull requests, which includes whether you employ large-language model (LLM)-based development tools.
So, we don't.
However, due to both legal and human reasons, we have to establish boundaries.
> [!CAUTION]
> **TL;DR:**
> - We take the responsibility for this project very seriously and we expect you to take your responsibility for your contributions seriously, too.
> This used to be a given, but it changed now that a pull request is just one prompt away.
>
> - Every contribution has to be backed by a human who unequivocally owns the copyright for all changes.
> No LLM bots in `Co-authored-by:`s.
>
> - DoS-by-slop leads to a permanent ban.
>
> - Absolutely **no** unsupervised agentic tools like OpenClaw.
>
> ---
>
> By submitting a pull request, you certify that:
>
> - You are the author of the contribution or have the legal right to submit it.
> - You either hold the copyright to the changes or have explicit legal authorization to contribute them under this project's license.
> - You understand the code.
> - You accept full responsibility for it.
## Legal
There is ongoing legal uncertainty regarding the copyright status of LLM-generated works and their provenance.
Since we do not have a formal [Contributor License Agreement](https://en.wikipedia.org/wiki/Contributor_license_agreement) (CLA), you retain your copyright to your changes to this project.
Therefore, allowing contributions by LLMs has unpredictable consequences for the copyright status of this project even when leaving aside possible copyright violations due to plagiarism.
## Human
As the makers of software that is used by millions of people worldwide and with a reputation for high-quality maintenance, we take our responsibility to our users very seriously.
No matter what LLM vendors or boosters on LinkedIn tell you, we have to manually review every change before merging, because it's **our responsibility** to keep the project stable.
Please understand that by opening low-quality pull requests you're not helping anyone.
Worse, you're [poisoning the open source ecosystem](https://lwn.net/Articles/1058266/) that was precarious even before the arrival of LLM tools.
Having to wade through plausible-looking-but-low-quality pull requests and trying to determine which ones are legit is extremely demoralizing and has already burned out many good maintainers.
Put bluntly, we have no time or interest to become part of your vibe coding loop where you drop LLM slop at our door, we spend time and energy to review it, and you just feed it back into the LLM for another iteration.
This dynamic is especially pernicious because it poisons the well for mentoring new contributors which we are committed to.
## Summary
In practice, this means:
- Pull requests that have an LLM product listed as co-author can't be merged and will be closed without further discussion.
We cannot risk the copyright status of this project.
If you used LLM tools during development, you may still submit but you must remove any LLM co-author tags and take full ownership of every line.
- By submitting a pull request, **you** take full **technical and legal** responsibility for the contents of the pull request and promise that **you** hold the copyright for the changes submitted.
"An LLM wrote it" is **not** an acceptable response to questions or critique.
**If you cannot explain and defend the changes you submit, do not submit them** and open a high-quality bug report/feature request instead.
- Accounts that exercise bot-like behavior like automated mass pull requests will be permanently banned, whether they belong to a human or not.
- Do **not** post LLM-generated review comments we can prompt LLMs ourselves should we desire their wisdom.
Do **not** post summaries unless you've fact-checked them and take responsibility for 100% of their content.
Remember that *all* LLM output *looks* **plausible**.
When using these tools, it's **your** responsibility to ensure that it's also **correct** and has a reasonable signal-to-noise ratio.
---
# Credits, Attribution
The original version of this can be found in [pyca/service-identity][] and is
used with permission from Hynek Schlawack.
[pyca/service-identity]:
https://github.com/pyca/service-identity/blob/fa91bf55cfda64145aa3d202cc84059befb98af4/.github/AI_POLICY.md

View File

@@ -1,8 +1,7 @@
# Contribution Guidelines
Before opening any issues or proposing any pull requests, please read
our [Contributor's Guide](https://requests.readthedocs.io/en/latest/dev/contributing/)
as well as our [AI Policy](/.github/AI_POLICY.md).
our [Contributor's Guide](https://requests.readthedocs.io/en/latest/dev/contributing/).
To get the greatest chance of helpful responses, please also observe the
following additional notes.
@@ -53,14 +52,3 @@ Please be aware of the following things when filing bug reports:
If you do not provide all of these things, it will take us much longer to
fix your problem. If we ask you to clarify these and you never respond, we
will close your issue without fixing it.
## Good Pull Requests
1. Avoid raising duplicate pull requests. *Please* use the GitHub pull request
search feature to check whether someone else has already filed a pull
request.
2. When filing a pull request, always include tests that fail without your
changes thus preventing the fix from regressing.
3. Always run the test suite locally
4. Write commit messages that explain _why_ a change was made beyond "Fixes
#NNNN". Why is this the right fix? What other options were ruled out? etc.

View File

@@ -19,9 +19,6 @@ updates:
directory: "/"
schedule:
interval: "weekly"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-patch"]
cooldown:
default-days: 7
groups:

View File

@@ -32,7 +32,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@@ -46,7 +46,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
languages: "python"
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -57,7 +57,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -71,4 +71,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1

View File

@@ -11,14 +11,12 @@ jobs:
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.x"
- name: Run pre-commit
run: |
python -m pip install pre-commit==4.6.0
pre-commit run --show-diff-on-failure --color=always --all-files
uses: pre-commit/action@646c83fcd040023954eafda54b4db0192ce70507 # v3.0.0

View File

@@ -13,7 +13,7 @@ jobs:
if: github.repository_owner == 'psf'
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:
issue-lock-inactive-days: 90
pr-lock-inactive-days: 90

View File

@@ -23,12 +23,12 @@ jobs:
steps:
- name: "Checkout repository"
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
uses: "actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd" # v6.0.2
with:
persist-credentials: false
- name: "Setup Python"
uses: "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" # v7.0.0
uses: "actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405" # v6.2.0
with:
python-version: "3.x"

View File

@@ -8,11 +8,12 @@ permissions:
jobs:
build:
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.python-version == '3.15-dev' }}
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15-dev", "pypy-3.11"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15-dev", "pypy-3.11"]
os: [ubuntu-22.04, macOS-latest, windows-latest]
# Pypy-3.11 can't install openssl-sys with rust
# which prevents us from testing in GHA.
@@ -20,11 +21,11 @@ jobs:
- { python-version: "pypy-3.11", os: "windows-latest" }
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
@@ -45,11 +46,11 @@ jobs:
fail-fast: true
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: 'Set up Python 3.10'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
with:
python-version: '3.10'
- name: Install dependencies
@@ -67,11 +68,11 @@ jobs:
fail-fast: true
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
with:
persist-credentials: false
- name: 'Set up Python 3.10'
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
with:
python-version: '3.10'
- name: Install dependencies

View File

@@ -14,12 +14,12 @@ jobs:
python-version: ["3.10", "3.14"]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}

View File

@@ -16,9 +16,9 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Run zizmor 🌈
uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2

View File

@@ -13,7 +13,7 @@ repos:
args: [--fix=auto]
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.4
rev: v0.15.12
hooks:
- id: ruff-check
args: [--fix]

View File

@@ -6,56 +6,19 @@ dev
- \[Short description of non-trivial change.\]
2.34.2 (2026-05-14)
2.34.0 (2026-05-??)
-------------------
- Moved `headers` input type back to `Mapping` to avoid invariance issues
with `MutableMapping` and inferred dict types. Users calling
`Request.headers.update()` may need to narrow typing in their code. (#7441)
2.34.1 (2026-05-13)
-------------------
**Bugfixes**
- Widened `json` input type from `dict` and `list` to `Mapping`
and `Sequence`. (#7436)
- Changed `headers` input type to MutableMapping and removed `None` from
`Request.headers` typing to improve handling for users. (#7431)
- `Response.reason` moved from `str | None` to `str` to improve handling
for users. (#7437)
- Fixed a bug where some bodies with custom `__getattr__` implementations
weren't being properly detected as Iterables. (#7433)
2.34.0 (2026-05-11)
-------------------
**Announcements**
- Requests 2.34.0 introduces inline types, replacing those provided by
typeshed. Public API types should be fully compatible with mypy, pyright,
and ty. We believe types are comprehensive but if you find issues, please
report them to the pinned tracking issue.
Special thanks to @bastimeyer, @cthoyt, @edgarrmondragon, and @srittau for
helping review and test the types ahead of the release. (#7272)
**Improvements**
- Digest Auth hashing algorithms have added `usedforsecurity=False` to clarify
* Requests 2.34.0 introduces inline types, replacing those provided by
typeshed. Public API types should be fully compatible with mypy, pyright,
and ty. (#7272)
* Digest Auth hashing algorithms have added `usedforsecurity=False` to clarify
security considerations. (#7310)
- Requests added support for Python 3.15 based on beta1. Downstream projects
should be able to start testing prior to its release in October. (#7422)
- Requests added support for Python 3.14t. (#7419)
**Bugfixes**
- ``Response.history`` no longer contains a reference to itself, preventing
* ``Response.history`` no longer contains a reference to itself, preventing
accidental looping when traversing the history list. (#7328)
- Requests no longer performs greedy matching on no_proxy domains. The
proxy_bypass implementation has been updated with CPython's fix from
bpo-39057. (#7427)
- Requests no longer incorrectly strips duplicate leading slashes in
URI paths. This should address user issues with specific presigned
URLs. Note the full fix requires urllib3 2.7.0+. (#7315)
2.33.1 (2026-03-30)

View File

@@ -9,7 +9,7 @@ BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.

View File

@@ -30,7 +30,7 @@ Why not Httplib2?
-----------------
Chris Adams gave an excellent summary on
`Hacker News <https://news.ycombinator.com/item?id=2884406>`_:
`Hacker News <http://news.ycombinator.com/item?id=2884406>`_:
httplib2 is part of why you should use requests: it's far more respectable
as a client but not as well documented and it still takes way too much code
@@ -44,7 +44,7 @@ Chris Adams gave an excellent summary on
Disclosure: I'm listed in the requests AUTHORS file but can claim credit
for, oh, about 0.0001% of the awesomeness.
1. https://code.google.com/p/httplib2/issues/detail?id=96 is a good example:
1. http://code.google.com/p/httplib2/issues/detail?id=96 is a good example:
an annoying bug that affected many people, there was a fix available for
months, which worked great when I applied it in a fork and pounded a couple
TB of data through it, but it took over a year to make it into trunk and

View File

@@ -7,4 +7,4 @@ Articles & Talks
- `Issac Kelly's 'Consuming Web APIs' talk <https://issackelly.github.io/Consuming-Web-APIs-with-Python-Talk/slides/slides.html>`_
- `Blog post about Requests via Yum <https://arunsag.wordpress.com/2011/08/17/new-package-python-requests-http-for-humans/>`_
- `Russian blog post introducing Requests <https://habr.com/post/126262/>`_
- `Sending JSON in Requests <https://www.coglib.com/~icordasc/blog/2014/11/sending-json-in-requests.html>`_
- `Sending JSON in Requests <http://www.coglib.com/~icordasc/blog/2014/11/sending-json-in-requests.html>`_

View File

@@ -14,7 +14,7 @@ getting a feel for how contributing to this project works. If you have any
questions, feel free to reach out to either `Nate Prewitt`_, `Ian Cordasco`_,
or `Seth Michael Larson`_, the primary maintainers.
.. _Ian Cordasco: https://www.coglib.com/~icordasc/
.. _Ian Cordasco: http://www.coglib.com/~icordasc/
.. _Nate Prewitt: https://www.nateprewitt.com/
.. _Seth Michael Larson: https://sethmlarson.dev/
@@ -132,8 +132,8 @@ files and a semi-formal, yet friendly and approachable, prose style.
When presenting Python code, use single-quoted strings (``'hello'`` instead of
``"hello"``).
.. _reStructuredText: https://docutils.sourceforge.net/rst.html
.. _Sphinx: https://www.sphinx-doc.org/en/master/
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
.. _Sphinx: http://sphinx-doc.org/index.html
.. _bug-reports:

View File

@@ -65,7 +65,7 @@ if errorlevel 9009 (
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
echo.http://sphinx-doc.org/
exit /b 1
)

View File

@@ -467,7 +467,6 @@ Let's print some request method arguments at runtime::
You can add multiple hooks to a single request. Let's call two hooks at once::
>>> r = requests.get('https://httpbin.org/', hooks={'response': [print_url, record_hook]})
https://httpbin.org/
>>> r.hook_called
True

View File

@@ -37,11 +37,9 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Programming Language :: Python :: Free Threading :: 2 - Beta",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Software Development :: Libraries"
]
@@ -69,9 +67,6 @@ test = [
typecheck = [
"pyright",
"typing_extensions",
"chardet",
"cryptography",
"PyOpenSSL",
]
[tool.setuptools]
@@ -118,8 +113,3 @@ testpaths = ["tests"]
[tool.pyright]
include = ["src/requests"]
typeCheckingMode = "strict"
# linting rules unrelated to type checking
reportPrivateUsage = false
reportPrivateImportUsage = false
reportUnnecessaryIsInstance = false
reportUnusedImport = false # duplicate of Ruff's F401 rule

View File

@@ -52,7 +52,7 @@ except ImportError:
charset_normalizer_version = None
try:
from chardet import __version__ as chardet_version
from chardet import __version__ as chardet_version # type: ignore[import-not-found]
except ImportError:
chardet_version = None
@@ -111,13 +111,13 @@ def _check_cryptography(cryptography_version: str) -> None:
# Check imported dependencies for compatibility.
try:
check_compatibility(
urllib3.__version__,
chardet_version,
urllib3.__version__, # type: ignore[reportPrivateImportUsage]
chardet_version, # type: ignore[reportUnknownArgumentType]
charset_normalizer_version,
)
except (AssertionError, ValueError):
warnings.warn(
f"urllib3 ({urllib3.__version__}) or chardet "
f"urllib3 ({urllib3.__version__}) or chardet " # type: ignore[reportPrivateImportUsage]
f"({chardet_version})/charset_normalizer ({charset_normalizer_version}) "
"doesn't match a supported version!",
RequestsDependencyWarning,
@@ -138,11 +138,11 @@ try:
pyopenssl.inject_into_urllib3()
# Check cryptography version
from cryptography import (
__version__ as cryptography_version,
from cryptography import ( # type: ignore[reportMissingImports]
__version__ as cryptography_version, # type: ignore[reportUnknownVariableType]
)
_check_cryptography(cryptography_version)
_check_cryptography(cryptography_version) # type: ignore[reportUnknownArgumentType]
except ImportError:
pass

View File

@@ -5,8 +5,8 @@
__title__ = "requests"
__description__ = "Python HTTP for Humans."
__url__ = "https://requests.readthedocs.io"
__version__ = "2.34.2"
__build__ = 0x023402
__version__ = "2.34.0.dev1"
__build__ = 0x023400
__author__ = "Kenneth Reitz"
__author_email__ = "me@kennethreitz.org"
__license__ = "Apache-2.0"

View File

@@ -9,7 +9,7 @@ by external code.
from __future__ import annotations
from collections.abc import Callable, Iterable, Mapping, MutableMapping, Sequence
from collections.abc import Callable, Iterable, Mapping, MutableMapping
from typing import (
TYPE_CHECKING,
Any,
@@ -20,8 +20,6 @@ from typing import (
)
_T_co = TypeVar("_T_co", covariant=True)
_KT_co = TypeVar("_KT_co", covariant=True)
_VT_co = TypeVar("_VT_co", covariant=True)
@runtime_checkable
@@ -29,14 +27,9 @@ class SupportsRead(Protocol[_T_co]):
def read(self, length: int = ..., /) -> _T_co: ...
def has_read(obj: Any) -> TypeIs[SupportsRead[str | bytes]]:
"""Check if obj supports read, including __getattr__ based proxies."""
return isinstance(obj, SupportsRead) or hasattr(obj, "read")
@runtime_checkable
class SupportsItems(Protocol[_KT_co, _VT_co]):
def items(self) -> Iterable[tuple[_KT_co, _VT_co]]: ...
class SupportsItems(Protocol):
def items(self) -> Iterable[tuple[Any, Any]]: ...
# These are needed at runtime for default_hooks() return type
@@ -86,7 +79,7 @@ if TYPE_CHECKING:
str | bytes | int | float | Iterable[str | bytes | int | float] | None
)
ParamsType: TypeAlias = (
SupportsItems[_ParamsMappingKeyType, _ParamsMappingValueType]
Mapping[_ParamsMappingKeyType, _ParamsMappingValueType]
| tuple[tuple[_ParamsMappingKeyType, _ParamsMappingValueType], ...]
| Iterable[tuple[_ParamsMappingKeyType, _ParamsMappingValueType]]
| str
@@ -94,7 +87,7 @@ if TYPE_CHECKING:
| None
)
KVDataType: TypeAlias = Iterable[tuple[Any, Any]] | SupportsItems[Any, Any]
KVDataType: TypeAlias = Iterable[tuple[Any, Any]] | Mapping[Any, Any]
RawDataType: TypeAlias = KVDataType | str | bytes
StreamDataType: TypeAlias = SupportsRead[str | bytes]
@@ -114,9 +107,10 @@ if TYPE_CHECKING:
bytes | str | Iterable[bytes | str] | SupportsRead[bytes | str] | None
)
HeadersType: TypeAlias = Mapping[str, str | bytes] | None
HeadersType: TypeAlias = CaseInsensitiveDict[str] | Mapping[str, str | bytes]
HeadersUpdateType: TypeAlias = Mapping[str, str | bytes | None]
CookiesType: TypeAlias = RequestsCookieJar | Mapping[str, str]
CookiesType: TypeAlias = RequestsCookieJar | CookieJar | None
# Building blocks for FilesType
_FileName: TypeAlias = str | None
@@ -143,26 +137,20 @@ if TYPE_CHECKING:
VerifyType: TypeAlias = bool | str
CertType: TypeAlias = str | tuple[str, str] | None
JsonType: TypeAlias = (
None
| bool
| int
| float
| str
| Sequence["JsonType"]
| Mapping[str, "JsonType"]
None | bool | int | float | str | list["JsonType"] | dict[str, "JsonType"]
)
# TypedDicts for Unpack kwargs (PEP 692)
class BaseRequestKwargs(TypedDict, total=False):
headers: HeadersType
headers: Mapping[str, str | bytes] | None
cookies: RequestsCookieJar | CookieJar | dict[str, str] | None
files: FilesType
auth: AuthType
timeout: TimeoutType
allow_redirects: bool
proxies: dict[str, str] | None
hooks: HooksInputType | None
hooks: HooksType
stream: bool | None
verify: VerifyType | None
cert: CertType

View File

@@ -9,7 +9,7 @@ and maintain connections.
from __future__ import annotations
import os.path
import socket # noqa: F401
import socket # noqa: F401 # type: ignore[reportUnusedImport]
import typing
import warnings
from typing import Any
@@ -33,7 +33,7 @@ from urllib3.util import Timeout as TimeoutSauce
from urllib3.util import parse_url
from urllib3.util.retry import Retry
from .auth import _basic_auth_str
from .auth import _basic_auth_str # type: ignore[reportPrivateUsage]
from .compat import basestring, urlparse
from .cookies import extract_cookies_to_jar
from .exceptions import (
@@ -190,14 +190,6 @@ class HTTPAdapter(BaseAdapter):
"_pool_block",
]
max_retries: Retry
config: dict[str, Any]
proxy_manager: dict[str, Any]
_pool_connections: int
_pool_maxsize: int
_pool_block: bool
poolmanager: _PoolManager
def __init__(
self,
pool_connections: int = DEFAULT_POOLSIZE,
@@ -206,18 +198,19 @@ class HTTPAdapter(BaseAdapter):
pool_block: bool = DEFAULT_POOLBLOCK,
) -> None:
if max_retries == DEFAULT_RETRIES:
self.max_retries = Retry(0, read=False)
self.max_retries: Retry = Retry(0, read=False)
else:
self.max_retries = Retry.from_int(max_retries)
self.config = {}
self.proxy_manager = {}
self.config: dict[str, Any] = {}
self.proxy_manager: dict[str, Any] = {}
super().__init__()
self._pool_connections = pool_connections
self._pool_maxsize = pool_maxsize
self._pool_block = pool_block
self._pool_connections: int = pool_connections
self._pool_maxsize: int = pool_maxsize
self._pool_block: bool = pool_block
self.poolmanager: _PoolManager
self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block)
def __getstate__(self) -> dict[str, Any]:
@@ -590,6 +583,8 @@ class HTTPAdapter(BaseAdapter):
using_socks_proxy = proxy_scheme.startswith("socks")
url = request.path_url
if url.startswith("//"): # Don't confuse urllib3
url = f"/{url.lstrip('/')}"
if is_proxied_http_request and not using_socks_proxy:
url = urldefragauth(request.url)

View File

@@ -41,7 +41,7 @@ def _basic_auth_str(username: bytes | str, password: bytes | str) -> str:
#
# These are here solely to maintain backwards compatibility
# for things like ints. This will be removed in 3.0.0.
if not isinstance(username, basestring): # runtime guard for non-str/bytes
if not isinstance(username, basestring): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for non-str/bytes
warnings.warn(
"Non-string usernames will no longer be supported in Requests "
f"3.0.0. Please convert the object you've passed in ({username!r}) to "
@@ -51,7 +51,7 @@ def _basic_auth_str(username: bytes | str, password: bytes | str) -> str:
)
username = str(username)
if not isinstance(password, basestring): # runtime guard for non-str/bytes
if not isinstance(password, basestring): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for non-str/bytes
warnings.warn(
"Non-string passwords will no longer be supported in Requests "
f"3.0.0. Please convert the object you've passed in ({type(password)!r}) to "
@@ -85,17 +85,14 @@ class AuthBase:
class HTTPBasicAuth(AuthBase):
"""Attaches HTTP Basic Authentication to the given Request object."""
username: bytes | str
password: bytes | str
@overload
def __init__(self, username: str, password: str) -> None: ...
@overload
def __init__(self, username: bytes, password: bytes) -> None: ...
def __init__(self, username: bytes | str, password: bytes | str) -> None:
self.username = username
self.password = password
self.username: bytes | str = username
self.password: bytes | str = password
def __eq__(self, other: object) -> bool:
return all(
@@ -124,25 +121,16 @@ class HTTPProxyAuth(HTTPBasicAuth):
class HTTPDigestAuth(AuthBase):
"""Attaches HTTP Digest Authentication to the given Request object."""
username: bytes | str
password: bytes | str
_thread_local: threading.local
last_nonce: str
nonce_count: int
chal: dict[str, str]
pos: int | None
num_401_calls: int | None
@overload
def __init__(self, username: str, password: str) -> None: ...
@overload
def __init__(self, username: bytes, password: bytes) -> None: ...
def __init__(self, username: bytes | str, password: bytes | str) -> None:
self.username = username
self.password = password
self.username: bytes | str = username
self.password: bytes | str = password
# Keep state in per-thread local storage
self._thread_local = threading.local()
self._thread_local: threading.local = threading.local()
def init_per_thread_state(self) -> None:
# Ensure state is initialized just once per-thread
@@ -300,7 +288,7 @@ class HTTPDigestAuth(AuthBase):
r.content
r.close()
prep = r.request.copy()
cookie_jar = cast("CookieJar", prep._cookies)
cookie_jar = cast("CookieJar", prep._cookies) # type: ignore[reportPrivateUsage]
extract_cookies_to_jar(cookie_jar, r.request, r.raw)
prep.prepare_cookies(cookie_jar)

View File

@@ -14,12 +14,13 @@ from __future__ import annotations
import importlib
import sys
from types import ModuleType
from typing import TYPE_CHECKING
# -------
# urllib3
# -------
from urllib3 import __version__ as urllib3_version
from urllib3 import (
__version__ as urllib3_version, # type: ignore[reportPrivateImportUsage]
)
# Detect which major version of urllib3 is being used.
try:
@@ -45,10 +46,7 @@ def _resolve_char_detection() -> ModuleType | None:
return chardet
if TYPE_CHECKING:
import chardet
else:
chardet = _resolve_char_detection()
chardet = _resolve_char_detection()
# -------
# Pythons

View File

@@ -40,13 +40,11 @@ class MockRequest:
probably want `get_cookie_header`, defined below.
"""
type: str
def __init__(self, request: PreparedRequest) -> None:
assert _is_prepared(request)
self._r = request
self._new_headers: dict[str, str] = {}
self.type = urlparse(self._r.url).scheme
self.type: str = urlparse(self._r.url).scheme
def get_type(self) -> str:
return self.type
@@ -610,7 +608,7 @@ def merge_cookies(
:param cookies: Dictionary or CookieJar object to be added.
:rtype: CookieJar
"""
if not isinstance(cookiejar, cookielib.CookieJar): # runtime guard
if not isinstance(cookiejar, cookielib.CookieJar): # type: ignore[reportUnnecessaryIsInstance] # runtime guard
raise ValueError("You can only merge into CookieJar")
if isinstance(cookies, dict):

View File

@@ -22,14 +22,11 @@ class RequestException(IOError):
request.
"""
response: Response | None
request: Request | PreparedRequest | None
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize RequestException with `request` and `response` objects."""
response: Response | None = kwargs.pop("response", None)
self.response = response
self.request = kwargs.pop("request", None)
self.response: Response | None = response
self.request: Request | PreparedRequest | None = kwargs.pop("request", None)
if response is not None and not self.request and hasattr(response, "request"):
self.request = response.request
super().__init__(*args, **kwargs)

View File

@@ -1,5 +1,7 @@
"""Module containing bug report helper(s)."""
# pyright: reportUnknownMemberType=false
import json
import platform
import ssl
@@ -17,7 +19,7 @@ except ImportError:
charset_normalizer = None
try:
import chardet
import chardet # type: ignore[import-not-found]
except ImportError:
chardet = None
@@ -28,8 +30,8 @@ except ImportError:
OpenSSL = None
cryptography = None
else:
import cryptography
import OpenSSL
import cryptography # type: ignore[import-not-found]
import OpenSSL # type: ignore[import-not-found]
def _implementation():
@@ -49,7 +51,7 @@ def _implementation():
implementation_version = platform.python_version()
elif implementation == "PyPy":
pypy = sys.pypy_version_info # type: ignore[attr-defined]
implementation_version = f"{pypy.major}.{pypy.minor}.{pypy.micro}" # pyright: ignore[reportUnknownMemberType]
implementation_version = f"{pypy.major}.{pypy.minor}.{pypy.micro}"
if sys.pypy_version_info.releaselevel != "final": # type: ignore[attr-defined]
implementation_version = "".join(
[implementation_version, sys.pypy_version_info.releaselevel] # type: ignore[attr-defined]
@@ -78,7 +80,7 @@ def info() -> dict[str, Any]:
}
implementation_info = _implementation()
urllib3_info = {"version": urllib3.__version__}
urllib3_info = {"version": urllib3.__version__} # type: ignore[reportPrivateImportUsage]
charset_normalizer_info = {"version": None}
chardet_info: dict[str, str | None] = {"version": None}
if charset_normalizer:

View File

@@ -12,7 +12,7 @@ import datetime
# Import encoding now, to avoid implicit import later.
# Implicit import within threads may cause LookupError when standard library is in a ZIP,
# such as in Embedded Python. See https://github.com/psf/requests/issues/3578.
import encodings.idna # noqa: F401
import encodings.idna # noqa: F401 # type: ignore[reportUnusedImport]
from collections.abc import Callable, Generator, Iterable, Iterator, Mapping
from io import UnsupportedOperation
from typing import (
@@ -35,8 +35,8 @@ from urllib3.fields import RequestField
from urllib3.filepost import encode_multipart_formdata
from urllib3.util import parse_url
from . import _types as _t
from ._internal_utils import to_native_string, unicode_is_ascii
from ._types import SupportsRead as _SupportsRead
from .auth import HTTPBasicAuth
from .compat import (
JSONDecodeError,
@@ -50,7 +50,7 @@ from .compat import (
)
from .compat import json as complexjson
from .cookies import (
_copy_cookie_jar,
_copy_cookie_jar, # type: ignore[reportPrivateUsage]
cookiejar_from_dict,
get_cookie_header,
)
@@ -87,6 +87,7 @@ if TYPE_CHECKING:
from typing_extensions import Self
from . import _types as _t
from .adapters import HTTPAdapter
from .cookies import RequestsCookieJar
@@ -160,7 +161,7 @@ class RequestEncodingMixin:
if isinstance(data, (str, bytes)):
return data
elif _t.has_read(data):
elif isinstance(data, _SupportsRead):
return data
elif hasattr(data, "__iter__"):
result: list[tuple[bytes, bytes]] = []
@@ -235,9 +236,9 @@ class RequestEncodingMixin:
if isinstance(fp, (str, bytes, bytearray)):
fdata = fp
elif _t.has_read(fp):
elif isinstance(fp, _SupportsRead): # type: ignore[reportUnnecessaryIsInstance] # defensive check for untyped callers
fdata = fp.read()
elif fp is None: # defensive check for untyped callers
elif fp is None: # type: ignore[reportUnnecessaryComparison] # defensive check for untyped callers
continue
else:
fdata = fp
@@ -265,9 +266,7 @@ class RequestHooksMixin:
if isinstance(hook, Callable):
self.hooks[event].append(hook)
elif hasattr(hook, "__iter__"):
self.hooks[event].extend(
h for h in hook if isinstance(h, Callable)
) # defensive runtime filter
self.hooks[event].extend(h for h in hook if isinstance(h, Callable)) # type: ignore[reportUnnecessaryIsInstance] # defensive runtime filter
def deregister_hook(self, event: str, hook: _t.HookType) -> bool:
"""Deregister a previously registered hook.
@@ -309,22 +308,11 @@ class Request(RequestHooksMixin):
<PreparedRequest [GET]>
"""
hooks: dict[str, list[_t.HookType]]
method: str | None
url: _t.UriType | None
headers: Mapping[str, str | bytes]
files: _t.FilesType
data: _t.DataType
json: _t.JsonType
params: _t.ParamsType
auth: _t.AuthType
cookies: RequestsCookieJar | CookieJar | dict[str, str] | None
def __init__(
self,
method: str | None = None,
url: _t.UriType | None = None,
headers: _t.HeadersType = None,
headers: Mapping[str, str | bytes] | None = None,
files: _t.FilesType = None,
data: _t.DataType = None,
params: _t.ParamsType = None,
@@ -340,19 +328,19 @@ class Request(RequestHooksMixin):
params = {} if params is None else params
hooks = {} if hooks is None else hooks
self.hooks = default_hooks()
self.hooks: dict[str, list[_t.HookType]] = default_hooks()
for k, v in list(hooks.items()):
self.register_hook(event=k, hook=v)
self.method = method
self.url = url
self.headers = headers
self.files = files
self.data = data
self.json = json
self.params = params
self.auth = auth
self.cookies = cookies
self.method: str | None = method
self.url: _t.UriType | None = url
self.headers: _t.HeadersType | None = headers
self.files: _t.FilesType = files
self.data: _t.DataType = data
self.json: _t.JsonType = json
self.params: _t.ParamsType = params
self.auth: _t.AuthType = auth
self.cookies: _t.CookiesType | dict[str, str] = cookies
def __repr__(self) -> str:
return f"<Request [{self.method}]>"
@@ -396,30 +384,22 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
<Response [200]>
"""
method: str | None
url: str | None
headers: CaseInsensitiveDict[str | bytes]
_cookies: RequestsCookieJar | CookieJar | None
body: _t.BodyType
hooks: dict[str, list[_t.HookType]]
_body_position: int | object | None
def __init__(self) -> None:
#: HTTP verb to send to the server.
self.method = None
self.method: str | None = None
#: HTTP URL to send the request to.
self.url = None
self.url: str | None = None
#: dictionary of HTTP headers.
self.headers = None # type: ignore[assignment]
self.headers: CaseInsensitiveDict[str | bytes] = None # type: ignore[assignment]
# The `CookieJar` used to create the Cookie header will be stored here
# after prepare_cookies is called
self._cookies = None
self._cookies: _t.CookiesType = None
#: request body to send to the server.
self.body = None
self.body: _t.BodyType = None
#: dictionary of callback hooks, for internal usage.
self.hooks = default_hooks()
self.hooks: dict[str, list[_t.HookType]] = default_hooks()
#: integer denoting starting position of a readable file-like body.
self._body_position = None
self._body_position: int | object | None = None
def prepare(
self,
@@ -598,9 +578,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
if not isinstance(body, bytes):
body = body.encode("utf-8")
# data that proxies attributes to underlying objects needs hasattr
is_iterable = isinstance(data, Iterable) or hasattr(data, "__iter__")
if is_iterable and not isinstance(data, (str, bytes, list, tuple, Mapping)):
if isinstance(data, Iterable) and not isinstance(
data, (str, bytes, list, tuple, Mapping)
):
try:
length = super_len(data)
except (TypeError, AttributeError, UnsupportedOperation):
@@ -638,7 +618,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
else:
if raw_data:
body = self._encode_params(raw_data)
if isinstance(data, basestring) or _t.has_read(data):
if isinstance(data, basestring) or isinstance(data, _SupportsRead):
content_type = None
else:
content_type = "application/x-www-form-urlencoded"
@@ -734,19 +714,6 @@ class Response:
server's response to an HTTP request.
"""
_content: bytes | Literal[False] | None
_content_consumed: bool
_next: PreparedRequest | None
status_code: int
headers: CaseInsensitiveDict[str]
raw: Any
url: str
encoding: str | None
history: list[Response]
reason: str
cookies: RequestsCookieJar
elapsed: datetime.timedelta
request: PreparedRequest
connection: HTTPAdapter
__attrs__: list[str] = [
@@ -763,39 +730,39 @@ class Response:
]
def __init__(self) -> None:
self._content = False
self._content_consumed = False
self._next = None
self._content: bytes | Literal[False] | None = False
self._content_consumed: bool = False
self._next: PreparedRequest | None = None
#: Integer Code of responded HTTP Status, e.g. 404 or 200.
self.status_code = None # type: ignore[assignment]
self.status_code: int = None # type: ignore[assignment]
#: Case-insensitive Dictionary of Response Headers.
#: For example, ``headers['content-encoding']`` will return the
#: value of a ``'Content-Encoding'`` response header.
self.headers = CaseInsensitiveDict()
self.headers: CaseInsensitiveDict[str] = CaseInsensitiveDict()
#: File-like object representation of response (for advanced usage).
#: Use of ``raw`` requires that ``stream=True`` be set on the request.
#: This requirement does not apply for use internally to Requests.
self.raw = None
self.raw: Any = None
#: Final URL location of Response.
self.url = None # type: ignore[assignment]
self.url: str = None # type: ignore[assignment]
#: Encoding to decode with when accessing r.text.
self.encoding = None
self.encoding: str | None = None
#: A list of :class:`Response <Response>` objects from
#: the history of the Request. Any redirect responses will end
#: up here. The list is sorted from the oldest to the most recent request.
self.history = []
self.history: list[Response] = []
#: Textual reason of responded HTTP Status, e.g. "Not Found" or "OK".
self.reason = None # type: ignore[assignment]
self.reason: str | None = None
#: A CookieJar of Cookies the server sent back.
self.cookies = cookiejar_from_dict({})
self.cookies: RequestsCookieJar = cookiejar_from_dict({})
#: The amount of time elapsed between sending the request
#: and the arrival of the response (as a timedelta).
@@ -803,11 +770,11 @@ class Response:
#: the first byte of the request and finishing parsing the headers. It
#: is therefore unaffected by consuming the response content or the
#: value of the ``stream`` keyword argument.
self.elapsed = datetime.timedelta(0)
self.elapsed: datetime.timedelta = datetime.timedelta(0)
#: The :class:`PreparedRequest <PreparedRequest>` object to which this
#: is a response.
self.request = None # type: ignore[assignment]
self.request: PreparedRequest = None # type: ignore[assignment]
def __enter__(self) -> Self:
return self
@@ -957,9 +924,7 @@ class Response:
if self._content_consumed and isinstance(self._content, bool):
raise StreamConsumedError()
elif chunk_size is not None and not isinstance(
chunk_size, int
): # runtime guard for untyped callers
elif chunk_size is not None and not isinstance(chunk_size, int): # type: ignore[reportUnnecessaryIsInstance] # runtime guard for untyped callers
raise TypeError(
f"chunk_size must be an int, it is instead a {type(chunk_size)}."
)
@@ -1014,7 +979,7 @@ class Response:
):
if pending is not None:
# TODO: remove cast after iter_lines rewrite
chunk = cast("str | bytes", pending + chunk)
chunk = cast("str | bytes", pending + chunk) # type: ignore[operator]
if delimiter:
lines = chunk.split(delimiter) # type: ignore[arg-type]

View File

@@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Any, cast
from ._internal_utils import to_native_string
from ._types import is_prepared as _is_prepared
from .adapters import HTTPAdapter
from .auth import _basic_auth_str
from .auth import _basic_auth_str # type: ignore[reportPrivateUsage]
from .compat import cookielib, urljoin, urlparse
from .cookies import (
RequestsCookieJar,
@@ -38,7 +38,7 @@ from .hooks import default_hooks, dispatch_hook
# formerly defined here, reexposed here for backward compatibility
from .models import ( # noqa: F401
DEFAULT_REDIRECT_LIMIT,
REDIRECT_STATI,
REDIRECT_STATI, # type: ignore[reportUnusedImport]
PreparedRequest,
Request,
Response,
@@ -54,7 +54,7 @@ from .utils import ( # noqa: F401
requote_uri,
resolve_proxies,
rewind_body,
should_bypass_proxies, # re-export for external consumers
should_bypass_proxies, # type: ignore[reportUnusedImport] # re-export for external consumers
to_key_val_list,
)
@@ -263,7 +263,7 @@ class SessionRedirectMixin:
# Extract any cookies sent on the response to the cookiejar
# in the new request. Because we've mutated our copied prepared
# request, use the old one that we haven't yet touched.
cookie_jar = cast("CookieJar", prepared_request._cookies)
cookie_jar = cast("CookieJar", prepared_request._cookies) # type: ignore[reportPrivateUsage]
extract_cookies_to_jar(cookie_jar, req, resp.raw)
merge_cookies(cookie_jar, self.cookies)
prepared_request.prepare_cookies(cookie_jar)
@@ -275,7 +275,7 @@ class SessionRedirectMixin:
# A failed tell() sets `_body_position` to `object()`. This non-None
# value ensures `rewindable` will be True, allowing us to raise an
# UnrewindableBodyError, instead of hanging the connection.
rewindable = prepared_request._body_position is not None and (
rewindable = prepared_request._body_position is not None and ( # type: ignore[reportPrivateUsage]
"Content-Length" in headers or "Transfer-Encoding" in headers
)
@@ -411,19 +411,6 @@ class Session(SessionRedirectMixin):
<Response [200]>
"""
headers: CaseInsensitiveDict[str]
auth: _t.AuthType
proxies: dict[str, str]
hooks: dict[str, list[_t.HookType]]
params: MutableMapping[str, Any]
stream: bool
verify: _t.VerifyType
cert: _t.CertType
max_redirects: int
trust_env: bool
cookies: RequestsCookieJar
adapters: MutableMapping[str, BaseAdapter]
__attrs__: list[str] = [
"headers",
"cookies",
@@ -443,27 +430,27 @@ class Session(SessionRedirectMixin):
#: A case-insensitive dictionary of headers to be sent on each
#: :class:`Request <Request>` sent from this
#: :class:`Session <Session>`.
self.headers = default_headers()
self.headers: CaseInsensitiveDict[str] = default_headers()
#: Default Authentication tuple or object to attach to
#: :class:`Request <Request>`.
self.auth = None
self.auth: _t.AuthType = None
#: Dictionary mapping protocol or protocol and host to the URL of the proxy
#: (e.g. {'http': 'foo.bar:3128', 'http://host.name': 'foo.bar:4012'}) to
#: be used on each :class:`Request <Request>`.
self.proxies = {}
self.proxies: dict[str, str] = {}
#: Event-handling hooks.
self.hooks = default_hooks()
self.hooks: dict[str, list[_t.HookType]] = default_hooks()
#: Dictionary of querystring data to attach to each
#: :class:`Request <Request>`. The dictionary values may be lists for
#: representing multivalued query parameters.
self.params = {}
self.params: MutableMapping[str, Any] = {}
#: Stream response content default.
self.stream = False
self.stream: bool = False
#: SSL Verification default.
#: Defaults to `True`, requiring requests to verify the TLS certificate at the
@@ -475,30 +462,30 @@ class Session(SessionRedirectMixin):
#: Only set this to `False` for testing.
#: If verify is set to a string, it must be the path to a CA bundle file
#: that will be used to verify the TLS certificate.
self.verify = True
self.verify: _t.VerifyType = True
#: SSL client certificate default, if String, path to ssl client
#: cert file (.pem). If Tuple, ('cert', 'key') pair.
self.cert = None
self.cert: _t.CertType = None
#: Maximum number of redirects allowed. If the request exceeds this
#: limit, a :class:`TooManyRedirects` exception is raised.
#: This defaults to requests.models.DEFAULT_REDIRECT_LIMIT, which is
#: 30.
self.max_redirects = DEFAULT_REDIRECT_LIMIT
self.max_redirects: int = DEFAULT_REDIRECT_LIMIT
#: Trust environment settings for proxy configuration, default
#: authentication and similar.
self.trust_env = True
self.trust_env: bool = True
#: A CookieJar containing all currently outstanding cookies set on this
#: session. By default it is a
#: :class:`RequestsCookieJar <requests.cookies.RequestsCookieJar>`, but
#: may be any other ``cookielib.CookieJar`` compatible object.
self.cookies = cookiejar_from_dict({})
self.cookies: RequestsCookieJar = cookiejar_from_dict({})
# Default connection adapters.
self.adapters = OrderedDict()
self.adapters: MutableMapping[str, BaseAdapter] = OrderedDict()
self.mount("https://", HTTPAdapter())
self.mount("http://", HTTPAdapter())
@@ -560,14 +547,14 @@ class Session(SessionRedirectMixin):
url: _t.UriType,
params: _t.ParamsType = None,
data: _t.DataType = None,
headers: _t.HeadersType = None,
headers: Mapping[str, str | bytes] | None = None,
cookies: RequestsCookieJar | CookieJar | dict[str, str] | None = None,
files: _t.FilesType = None,
auth: _t.AuthType = None,
timeout: _t.TimeoutType = None,
allow_redirects: bool = True,
proxies: dict[str, str] | None = None,
hooks: _t.HooksInputType | None = None,
hooks: _t.HooksType = None,
stream: bool | None = None,
verify: _t.VerifyType | None = None,
cert: _t.CertType = None,
@@ -652,23 +639,16 @@ class Session(SessionRedirectMixin):
return resp
def get(
self,
url: _t.UriType,
params: _t.ParamsType = None,
**kwargs: Unpack[_t.GetKwargs],
) -> Response:
def get(self, url: _t.UriType, **kwargs: Unpack[_t.GetKwargs]) -> Response:
r"""Sends a GET request. Returns :class:`Response` object.
:param url: URL for the new :class:`Request` object.
:param params: (optional) Dictionary, list of tuples or bytes to send
in the query string for the :class:`Request`.
:param \*\*kwargs: Optional arguments that ``request`` takes.
:rtype: requests.Response
"""
kwargs.setdefault("allow_redirects", True)
return self.request("GET", url, params=params, **kwargs)
return self.request("GET", url, **kwargs)
def options(self, url: _t.UriType, **kwargs: Unpack[_t.RequestKwargs]) -> Response:
r"""Sends a OPTIONS request. Returns :class:`Response` object.

View File

@@ -44,14 +44,12 @@ class CaseInsensitiveDict(MutableMapping[str, _VT], Generic[_VT]):
behavior is undefined.
"""
_store: OrderedDict[str, tuple[str, _VT]]
def __init__(
self,
data: Mapping[str, _VT] | Iterable[tuple[str, _VT]] | None = None,
**kwargs: _VT,
) -> None:
self._store = OrderedDict()
self._store: OrderedDict[str, tuple[str, _VT]] = OrderedDict()
if data is None:
data = {}
self.update(data, **kwargs)
@@ -96,10 +94,8 @@ class CaseInsensitiveDict(MutableMapping[str, _VT], Generic[_VT]):
class LookupDict(dict[str, _VT]):
"""Dictionary lookup object."""
name: Any
def __init__(self, name: Any = None) -> None:
self.name = name
self.name: bytes | str = name
super().__init__()
def __repr__(self) -> str:

View File

@@ -37,10 +37,10 @@ from .__version__ import __version__
# to_native_string is unused here, but imported here for backwards compatibility
from ._internal_utils import ( # noqa: F401
_HEADER_VALIDATORS_BYTE,
_HEADER_VALIDATORS_STR,
HEADER_VALIDATORS,
to_native_string,
_HEADER_VALIDATORS_BYTE, # type: ignore[reportPrivateUsage]
_HEADER_VALIDATORS_STR, # type: ignore[reportPrivateUsage]
HEADER_VALIDATORS, # type: ignore[reportUnusedImport]
to_native_string, # type: ignore[reportUnusedImport]
)
from ._types import SupportsItems as _SupportsItems
from .compat import (
@@ -147,7 +147,7 @@ if sys.platform == "win32":
def dict_to_sequence(
d: _t.SupportsItems[Any, Any] | Iterable[tuple[Any, Any]],
d: _t.SupportsItems | Iterable[tuple[Any, Any]],
) -> Iterable[tuple[Any, Any]]:
"""Returns an internal sequence dictionary update."""
@@ -371,10 +371,10 @@ def from_key_val_list(
def to_key_val_list(value: None) -> None: ...
@overload
def to_key_val_list(
value: _t.SupportsItems[_KT, _VT] | Iterable[tuple[_KT, _VT]],
value: Mapping[_KT, _VT] | Iterable[tuple[_KT, _VT]],
) -> list[tuple[_KT, _VT]]: ...
def to_key_val_list(
value: _t.SupportsItems[_KT, _VT] | Iterable[tuple[_KT, _VT]] | None,
value: Mapping[_KT, _VT] | Iterable[tuple[_KT, _VT]] | None,
) -> list[tuple[_KT, _VT]] | None:
"""Take an object and test to see if it can be represented as a
dictionary. If it can be, return a list of tuples, e.g.,
@@ -851,11 +851,9 @@ def should_bypass_proxies(url: str, no_proxy: str | None) -> bool:
host_with_port += f":{parsed.port}"
for host in no_proxy_hosts:
host = host.lstrip(".")
if hostname == host or host_with_port == host:
return True
host = "." + host
if hostname.endswith(host) or host_with_port.endswith(host):
# The URL does match something in no_proxy, so we don't want
# to apply the proxies on this URL.
return True
with set_environ("no_proxy", no_proxy_arg):
@@ -1102,7 +1100,7 @@ def _validate_header_part(
) -> None:
if isinstance(header_part, str):
validator = _HEADER_VALIDATORS_STR[header_validator_index]
elif isinstance(header_part, bytes):
elif isinstance(header_part, bytes): # type: ignore[reportUnnecessaryIsInstance]
# runtime guard for non-str/bytes input
validator = _HEADER_VALIDATORS_BYTE[header_validator_index]
else:
@@ -1142,11 +1140,11 @@ def rewind_body(prepared_request: PreparedRequest) -> None:
"""
body_seek = getattr(prepared_request.body, "seek", None)
if body_seek is not None and isinstance(
prepared_request._body_position,
prepared_request._body_position, # type: ignore[reportPrivateUsage]
integer_types,
):
try:
body_seek(prepared_request._body_position)
body_seek(prepared_request._body_position) # type: ignore[reportPrivateUsage]
except OSError:
raise UnrewindableBodyError(
"An error occurred when rewinding request body for redirect."

View File

@@ -22,15 +22,6 @@ def prepare_url(value):
return inner
@pytest.fixture(autouse=True)
def clean_proxy_environ(monkeypatch):
"""Remove proxy related environment variables for every test."""
proxy_vars = ("http_proxy", "https_proxy", "no_proxy", "ftp_proxy", "all_proxy")
for var in proxy_vars:
monkeypatch.delenv(var, raising=False)
monkeypatch.delenv(var.upper(), raising=False)
@pytest.fixture
def httpbin(httpbin):
return prepare_url(httpbin)

View File

@@ -1,8 +1,8 @@
import requests.adapters
def test_request_url_handles_leading_path_separators():
def test_request_url_trims_leading_path_separators():
"""See also https://github.com/psf/requests/issues/6643."""
a = requests.adapters.HTTPAdapter()
p = requests.Request(method="GET", url="http://127.0.0.1:10000//v:h").prepare()
assert "//v:h" == a.request_url(p, {})
assert "/v:h" == a.request_url(p, {})

View File

@@ -1087,32 +1087,6 @@ class TestRequests:
)
assert r.status_code == 200
def test_post_named_tempfile(self, httpbin):
with tempfile.NamedTemporaryFile(mode="w+") as f:
f.write("named temp file contents\n")
f.seek(0)
r = requests.post(
httpbin("post"),
files={"file": f},
)
assert r.status_code == 200
assert r.json()["files"]["file"] == "named temp file contents\n"
def test_post_getattr_proxy_read_only(self, httpbin):
class ReadProxy:
def __init__(self):
self._file = io.BytesIO(b"streamed body")
def __getattr__(self, name):
if name == "__iter__":
raise AttributeError(name)
return getattr(self._file, name)
r = requests.post(httpbin("post"), data=ReadProxy())
assert r.status_code == 200
assert r.json()["data"] == "streamed body"
@pytest.mark.parametrize(
"data",
(
@@ -2099,21 +2073,6 @@ class TestRequests:
assert "Unable to rewind request body" in str(e)
def test_getattr_proxy_stream_follows_redirect(self, httpbin):
"""Ensure stream wrappers that don't implement __iter__ directly are still detected."""
class AttrProxy:
def __init__(self):
self._file = io.BytesIO(b"data")
def __getattr__(self, name):
return getattr(self._file, name)
r = requests.post(
httpbin("redirect-to?url=/post&status_code=307"), data=AttrProxy()
)
assert r.json()["data"] == "data"
def _patch_adapter_gzipped_redirect(self, session, url):
adapter = session.get_adapter(url=url)
org_build_response = adapter.build_response

View File

@@ -844,29 +844,6 @@ def test_should_bypass_proxies_no_proxy(url, expected, monkeypatch):
assert should_bypass_proxies(url, no_proxy=no_proxy) == expected
@pytest.mark.parametrize(
"url, expected",
(
("http://localhost/", True),
("http://anotherdomain.com:8888/", True),
("http://newdomain.com:1234/", True),
("http://www.newdomain.com:1234/", True),
("http://foo.d.o.t/", True),
("http://d.o.t/", True),
("http://prelocalhost/", False),
("http://newdomain.com/", False),
("http://newdomain.com:1235/", False),
),
)
def test_should_bypass_proxies_no_proxy_domain_boundary(url, expected):
"""Ensure no_proxy matching respects domain boundaries and does not
greedily match domains that merely endswith the no_proxy entry.
See CPython bpo-39057.
"""
no_proxy = "localhost, anotherdomain.com, newdomain.com:1234, .d.o.t"
assert should_bypass_proxies(url, no_proxy=no_proxy) == expected
@pytest.mark.skipif(os.name != "nt", reason="Test only on Windows")
@pytest.mark.parametrize(
"url, expected, override",