Skip to content

Commit 0f5ef1b

Browse files
committed
Merge branch 'main' of github.com:psf/requests into 3.13
2 parents 2e14522 + f12ccbe commit 0f5ef1b

File tree

10 files changed

+169
-66
lines changed

10 files changed

+169
-66
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545

4646
# Initializes the CodeQL tools for scanning.
4747
- name: Initialize CodeQL
48-
uses: github/codeql-action/init@df5a14dc28094dc936e103b37d749c6628682b60 # v3.25.0
48+
uses: github/codeql-action/init@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
4949
with:
5050
languages: "python"
5151
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
5656
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
5757
# If this step fails, then you should remove it and run the build manually (see below)
5858
- name: Autobuild
59-
uses: github/codeql-action/autobuild@df5a14dc28094dc936e103b37d749c6628682b60 # v3.25.0
59+
uses: github/codeql-action/autobuild@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0
6060

6161
# ℹ️ Command-line programs to run using the OS shell.
6262
# 📚 https://git.io/JvXDl
@@ -70,4 +70,4 @@ jobs:
7070
# make release
7171

7272
- name: Perform CodeQL Analysis
73-
uses: github/codeql-action/analyze@df5a14dc28094dc936e103b37d749c6628682b60 # v3.25.0
73+
uses: github/codeql-action/analyze@eb055d739abdc2e8de2e5f4ba1a8b246daa779aa # v3.26.0

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
steps:
1414
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
1515
- name: Set up Python
16-
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
16+
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
1717
with:
1818
python-version: "3.x"
1919
- name: Run pre-commit

.github/workflows/run-tests.yml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
steps:
2828
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
2929
- name: Set up Python ${{ matrix.python-version }}
30-
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
30+
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
3131
with:
3232
python-version: ${{ matrix.python-version }}
3333
cache: 'pip'
@@ -47,7 +47,7 @@ jobs:
4747
steps:
4848
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
4949
- name: 'Set up Python 3.8'
50-
uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d
50+
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3
5151
with:
5252
python-version: '3.8'
5353
- name: Install dependencies
@@ -57,3 +57,23 @@ jobs:
5757
- name: Run tests
5858
run: |
5959
make ci
60+
61+
urllib3:
62+
name: 'urllib3 1.x'
63+
runs-on: 'ubuntu-latest'
64+
strategy:
65+
fail-fast: true
66+
67+
steps:
68+
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
69+
- name: 'Set up Python 3.8'
70+
uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3
71+
with:
72+
python-version: '3.8'
73+
- name: Install dependencies
74+
run: |
75+
make
76+
python -m pip install "urllib3<2"
77+
- name: Run tests
78+
run: |
79+
make ci

HISTORY.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ dev
66

77
- \[Short description of non-trivial change.\]
88

9+
2.32.3 (2024-05-29)
10+
-------------------
11+
12+
**Bugfixes**
13+
- Fixed bug breaking the ability to specify custom SSLContexts in sub-classes of
14+
HTTPAdapter. (#6716)
15+
- Fixed issue where Requests started failing to run on Python versions compiled
16+
without the `ssl` module. (#6724)
17+
918
2.32.2 (2024-05-21)
1019
-------------------
1120

setup.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from codecs import open
55

66
from setuptools import setup
7-
from setuptools.command.test import test as TestCommand
87

98
CURRENT_PYTHON = sys.version_info[:2]
109
REQUIRED_PYTHON = (3, 8)
@@ -28,30 +27,6 @@
2827
sys.exit(1)
2928

3029

31-
class PyTest(TestCommand):
32-
user_options = [("pytest-args=", "a", "Arguments to pass into py.test")]
33-
34-
def initialize_options(self):
35-
TestCommand.initialize_options(self)
36-
try:
37-
from multiprocessing import cpu_count
38-
39-
self.pytest_args = ["-n", str(cpu_count()), "--boxed"]
40-
except (ImportError, NotImplementedError):
41-
self.pytest_args = ["-n", "1", "--boxed"]
42-
43-
def finalize_options(self):
44-
TestCommand.finalize_options(self)
45-
self.test_args = []
46-
self.test_suite = True
47-
48-
def run_tests(self):
49-
import pytest
50-
51-
errno = pytest.main(self.pytest_args)
52-
sys.exit(errno)
53-
54-
5530
# 'setup.py publish' shortcut.
5631
if sys.argv[-1] == "publish":
5732
os.system("python setup.py sdist bdist_wheel")
@@ -118,7 +93,6 @@ def run_tests(self):
11893
"Topic :: Internet :: WWW/HTTP",
11994
"Topic :: Software Development :: Libraries",
12095
],
121-
cmdclass={"test": PyTest},
12296
tests_require=test_requirements,
12397
extras_require={
12498
"security": [],

src/requests/__version__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
__title__ = "requests"
66
__description__ = "Python HTTP for Humans."
77
__url__ = "https://requests.readthedocs.io"
8-
__version__ = "2.32.2"
9-
__build__ = 0x023202
8+
__version__ = "2.32.3"
9+
__build__ = 0x023203
1010
__author__ = "Kenneth Reitz"
1111
__author_email__ = "[email protected]"
1212
__license__ = "Apache-2.0"

src/requests/adapters.py

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,26 +73,44 @@ def SOCKSProxyManager(*args, **kwargs):
7373
DEFAULT_RETRIES = 0
7474
DEFAULT_POOL_TIMEOUT = None
7575

76-
_preloaded_ssl_context = create_urllib3_context()
77-
_preloaded_ssl_context.load_verify_locations(
78-
extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
79-
)
76+
77+
try:
78+
import ssl # noqa: F401
79+
80+
_preloaded_ssl_context = create_urllib3_context()
81+
_preloaded_ssl_context.load_verify_locations(
82+
extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
83+
)
84+
except ImportError:
85+
# Bypass default SSLContext creation when Python
86+
# interpreter isn't built with the ssl module.
87+
_preloaded_ssl_context = None
8088

8189

8290
def _urllib3_request_context(
8391
request: "PreparedRequest",
8492
verify: "bool | str | None",
8593
client_cert: "typing.Tuple[str, str] | str | None",
94+
poolmanager: "PoolManager",
8695
) -> "(typing.Dict[str, typing.Any], typing.Dict[str, typing.Any])":
8796
host_params = {}
8897
pool_kwargs = {}
8998
parsed_request_url = urlparse(request.url)
9099
scheme = parsed_request_url.scheme.lower()
91100
port = parsed_request_url.port
101+
102+
# Determine if we have and should use our default SSLContext
103+
# to optimize performance on standard requests.
104+
poolmanager_kwargs = getattr(poolmanager, "connection_pool_kw", {})
105+
has_poolmanager_ssl_context = poolmanager_kwargs.get("ssl_context")
106+
should_use_default_ssl_context = (
107+
_preloaded_ssl_context is not None and not has_poolmanager_ssl_context
108+
)
109+
92110
cert_reqs = "CERT_REQUIRED"
93111
if verify is False:
94112
cert_reqs = "CERT_NONE"
95-
elif verify is True:
113+
elif verify is True and should_use_default_ssl_context:
96114
pool_kwargs["ssl_context"] = _preloaded_ssl_context
97115
elif isinstance(verify, str):
98116
if not os.path.isdir(verify):
@@ -375,23 +393,83 @@ def build_response(self, req, resp):
375393

376394
return response
377395

396+
def build_connection_pool_key_attributes(self, request, verify, cert=None):
397+
"""Build the PoolKey attributes used by urllib3 to return a connection.
398+
399+
This looks at the PreparedRequest, the user-specified verify value,
400+
and the value of the cert parameter to determine what PoolKey values
401+
to use to select a connection from a given urllib3 Connection Pool.
402+
403+
The SSL related pool key arguments are not consistently set. As of
404+
this writing, use the following to determine what keys may be in that
405+
dictionary:
406+
407+
* If ``verify`` is ``True``, ``"ssl_context"`` will be set and will be the
408+
default Requests SSL Context
409+
* If ``verify`` is ``False``, ``"ssl_context"`` will not be set but
410+
``"cert_reqs"`` will be set
411+
* If ``verify`` is a string, (i.e., it is a user-specified trust bundle)
412+
``"ca_certs"`` will be set if the string is not a directory recognized
413+
by :py:func:`os.path.isdir`, otherwise ``"ca_certs_dir"`` will be
414+
set.
415+
* If ``"cert"`` is specified, ``"cert_file"`` will always be set. If
416+
``"cert"`` is a tuple with a second item, ``"key_file"`` will also
417+
be present
418+
419+
To override these settings, one may subclass this class, call this
420+
method and use the above logic to change parameters as desired. For
421+
example, if one wishes to use a custom :py:class:`ssl.SSLContext` one
422+
must both set ``"ssl_context"`` and based on what else they require,
423+
alter the other keys to ensure the desired behaviour.
424+
425+
:param request:
426+
The PreparedReqest being sent over the connection.
427+
:type request:
428+
:class:`~requests.models.PreparedRequest`
429+
:param verify:
430+
Either a boolean, in which case it controls whether
431+
we verify the server's TLS certificate, or a string, in which case it
432+
must be a path to a CA bundle to use.
433+
:param cert:
434+
(optional) Any user-provided SSL certificate for client
435+
authentication (a.k.a., mTLS). This may be a string (i.e., just
436+
the path to a file which holds both certificate and key) or a
437+
tuple of length 2 with the certificate file path and key file
438+
path.
439+
:returns:
440+
A tuple of two dictionaries. The first is the "host parameters"
441+
portion of the Pool Key including scheme, hostname, and port. The
442+
second is a dictionary of SSLContext related parameters.
443+
"""
444+
return _urllib3_request_context(request, verify, cert, self.poolmanager)
445+
378446
def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None):
379447
"""Returns a urllib3 connection for the given request and TLS settings.
380448
This should not be called from user code, and is only exposed for use
381449
when subclassing the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
382450
383-
:param request: The :class:`PreparedRequest <PreparedRequest>` object
384-
to be sent over the connection.
385-
:param verify: Either a boolean, in which case it controls whether
386-
we verify the server's TLS certificate, or a string, in which case it
387-
must be a path to a CA bundle to use.
388-
:param proxies: (optional) The proxies dictionary to apply to the request.
389-
:param cert: (optional) Any user-provided SSL certificate to be trusted.
390-
:rtype: urllib3.ConnectionPool
451+
:param request:
452+
The :class:`PreparedRequest <PreparedRequest>` object to be sent
453+
over the connection.
454+
:param verify:
455+
Either a boolean, in which case it controls whether we verify the
456+
server's TLS certificate, or a string, in which case it must be a
457+
path to a CA bundle to use.
458+
:param proxies:
459+
(optional) The proxies dictionary to apply to the request.
460+
:param cert:
461+
(optional) Any user-provided SSL certificate to be used for client
462+
authentication (a.k.a., mTLS).
463+
:rtype:
464+
urllib3.ConnectionPool
391465
"""
392466
proxy = select_proxy(request.url, proxies)
393467
try:
394-
host_params, pool_kwargs = _urllib3_request_context(request, verify, cert)
468+
host_params, pool_kwargs = self.build_connection_pool_key_attributes(
469+
request,
470+
verify,
471+
cert,
472+
)
395473
except ValueError as e:
396474
raise InvalidURL(e, request=request)
397475
if proxy:

src/requests/compat.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@
1010
import importlib
1111
import sys
1212

13+
# -------
14+
# urllib3
15+
# -------
16+
from urllib3 import __version__ as urllib3_version
17+
18+
# Detect which major version of urllib3 is being used.
19+
try:
20+
is_urllib3_1 = int(urllib3_version.split(".")[0]) == 1
21+
except (TypeError, AttributeError):
22+
# If we can't discern a version, prefer old functionality.
23+
is_urllib3_1 = True
24+
1325
# -------------------
1426
# Character Detection
1527
# -------------------

src/requests/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
getproxies,
3939
getproxies_environment,
4040
integer_types,
41+
is_urllib3_1,
4142
)
4243
from .compat import parse_http_list as _parse_list_header
4344
from .compat import (
@@ -136,7 +137,9 @@ def super_len(o):
136137
total_length = None
137138
current_position = 0
138139

139-
if isinstance(o, str):
140+
if not is_urllib3_1 and isinstance(o, str):
141+
# urllib3 2.x+ treats all strings as utf-8 instead
142+
# of latin-1 (iso-8859-1) like http.client.
140143
o = o.encode("utf-8")
141144

142145
if hasattr(o, "__len__"):

tests/test_requests.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
builtin_str,
2626
cookielib,
2727
getproxies,
28+
is_urllib3_1,
2829
urlparse,
2930
)
3031
from requests.cookies import cookiejar_from_dict, morsel_to_cookie
@@ -1810,23 +1811,6 @@ def test_autoset_header_values_are_native(self, httpbin):
18101811

18111812
assert p.headers["Content-Length"] == length
18121813

1813-
def test_content_length_for_bytes_data(self, httpbin):
1814-
data = "This is a string containing multi-byte UTF-8 ☃️"
1815-
encoded_data = data.encode("utf-8")
1816-
length = str(len(encoded_data))
1817-
req = requests.Request("POST", httpbin("post"), data=encoded_data)
1818-
p = req.prepare()
1819-
1820-
assert p.headers["Content-Length"] == length
1821-
1822-
def test_content_length_for_string_data_counts_bytes(self, httpbin):
1823-
data = "This is a string containing multi-byte UTF-8 ☃️"
1824-
length = str(len(data.encode("utf-8")))
1825-
req = requests.Request("POST", httpbin("post"), data=data)
1826-
p = req.prepare()
1827-
1828-
assert p.headers["Content-Length"] == length
1829-
18301814
def test_nonhttp_schemes_dont_check_URLs(self):
18311815
test_urls = (
18321816
"data:image/gif;base64,R0lGODlhAQABAHAAACH5BAUAAAAALAAAAAABAAEAAAICRAEAOw==",
@@ -2966,6 +2950,29 @@ def response_handler(sock):
29662950
assert client_cert is not None
29672951

29682952

2953+
def test_content_length_for_bytes_data(httpbin):
2954+
data = "This is a string containing multi-byte UTF-8 ☃️"
2955+
encoded_data = data.encode("utf-8")
2956+
length = str(len(encoded_data))
2957+
req = requests.Request("POST", httpbin("post"), data=encoded_data)
2958+
p = req.prepare()
2959+
2960+
assert p.headers["Content-Length"] == length
2961+
2962+
2963+
@pytest.mark.skipif(
2964+
is_urllib3_1,
2965+
reason="urllib3 2.x encodes all strings to utf-8, urllib3 1.x uses latin-1",
2966+
)
2967+
def test_content_length_for_string_data_counts_bytes(httpbin):
2968+
data = "This is a string containing multi-byte UTF-8 ☃️"
2969+
length = str(len(data.encode("utf-8")))
2970+
req = requests.Request("POST", httpbin("post"), data=data)
2971+
p = req.prepare()
2972+
2973+
assert p.headers["Content-Length"] == length
2974+
2975+
29692976
def test_json_decode_errors_are_serializable_deserializable():
29702977
json_decode_error = requests.exceptions.JSONDecodeError(
29712978
"Extra data",

0 commit comments

Comments
 (0)