diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 6c0f83b4e3..2db1d59a72 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,14 +9,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] os: [ubuntu-18.04, macOS-latest, windows-latest] include: - # pypy3 on Mac OS currently fails trying to compile + # pypy-3.7 on Mac OS currently fails trying to compile # brotlipy. Moving pypy3 to only test linux. - - python-version: pypy3 - os: ubuntu-latest - experimental: false - python-version: pypy-3.7 os: ubuntu-latest experimental: false diff --git a/HISTORY.md b/HISTORY.md index f712a84fb3..3a3c80e39a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -11,6 +11,11 @@ dev - Fixed urllib3 exception leak, wrapping `urllib3.exceptions.SSLError` with `requests.exceptions.SSLError` for `content` and `iter_content`. +**Deprecations** + +- ⚠️ Requests has officially dropped support for Python 2.7. ⚠️ +- Requests has officially dropped support for Python 3.6 (including pypy3). + 2.27.1 (2022-01-05) ------------------- diff --git a/README.md b/README.md index 807215ac56..c90ef08a5e 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Requests is available on PyPI: $ python -m pip install requests ``` -Requests officially supports Python 2.7 & 3.6+. +Requests officially supports Python 3.7+. ## Supported Features & Best–Practices diff --git a/docs/community/faq.rst b/docs/community/faq.rst index a72fa4fb40..9d900be2b5 100644 --- a/docs/community/faq.rst +++ b/docs/community/faq.rst @@ -55,16 +55,16 @@ Chris Adams gave an excellent summary on Python 3 Support? ----------------- -Yes! Requests officially supports Python 2.7 & 3.6+ and PyPy. +Yes! Requests officially supports Python 3.7+ and PyPy. Python 2 Support? ----------------- -Yes! We understand that we have a large user base with varying needs. Through -**at least** Requests 2.27.x, we will be providing continued support for Python -2.7. However, this support is likely to end some time in 2022. +No! As of Requests 2.28.0, Requests no longer supports Python 2.7. Users who +have been unable to migrate should pin to `requests<2.28`. Full information +can be found in `psf/requests#6023 `_. -It is *highly* recommended users migrate to Python 3.7+ now since Python +It is *highly* recommended users migrate to Python 3.8+ now since Python 2.7 is no longer receiving bug fixes or security updates as of January 1, 2020. What are "hostname doesn't match" errors? @@ -83,10 +83,7 @@ when servers are using `Virtual Hosting`_. When such servers are hosting more than one SSL site they need to be able to return the appropriate certificate based on the hostname the client is connecting to. -Python3 and Python 2.7.9+ include native support for SNI in their SSL modules. -For information on using SNI with Requests on Python < 2.7.9 refer to this -`Stack Overflow answer`_. +Python 3 already includes native support for SNI in their SSL modules. .. _`Server-Name-Indication`: https://en.wikipedia.org/wiki/Server_Name_Indication .. _`virtual hosting`: https://en.wikipedia.org/wiki/Virtual_hosting -.. _`Stack Overflow answer`: https://stackoverflow.com/questions/18578439/using-requests-with-tls-doesnt-give-sni-support/18579484#18579484 diff --git a/docs/index.rst b/docs/index.rst index dbcaa55740..306b60f3ea 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -72,7 +72,7 @@ Requests is ready for today's web. - Chunked Requests - ``.netrc`` Support -Requests officially supports Python 2.7 & 3.6+, and runs great on PyPy. +Requests officially supports Python 3.7+, and runs great on PyPy. The User Guide diff --git a/requests/_internal_utils.py b/requests/_internal_utils.py index 759d9a56ba..ebab39ff1c 100644 --- a/requests/_internal_utils.py +++ b/requests/_internal_utils.py @@ -8,7 +8,7 @@ which depend on extremely few external helpers (such as compat) """ -from .compat import is_py2, builtin_str, str +from .compat import builtin_str def to_native_string(string, encoding='ascii'): @@ -19,10 +19,7 @@ def to_native_string(string, encoding='ascii'): if isinstance(string, builtin_str): out = string else: - if is_py2: - out = string.encode(encoding) - else: - out = string.decode(encoding) + out = string.decode(encoding) return out diff --git a/requests/adapters.py b/requests/adapters.py index fe22ff450e..dc532999df 100644 --- a/requests/adapters.py +++ b/requests/adapters.py @@ -477,12 +477,7 @@ def send(self, request, stream=False, timeout=None, verify=True, cert=None, prox low_conn.send(b'0\r\n\r\n') # Receive the response from the server - try: - # For Python 2.7, use buffering of HTTP responses - r = low_conn.getresponse(buffering=True) - except TypeError: - # For compatibility with Python 3.3+ - r = low_conn.getresponse() + r = low_conn.getresponse() resp = HTTPResponse.from_httplib( r, diff --git a/requests/compat.py b/requests/compat.py index 029ae62ac3..aca7d58409 100644 --- a/requests/compat.py +++ b/requests/compat.py @@ -4,8 +4,9 @@ requests.compat ~~~~~~~~~~~~~~~ -This module handles import compatibility issues between Python 2 and -Python 3. +This module previously handled import compatibility issues +between Python 2 and Python 3. It remains for backwards +compatibility until the next major version. """ try: @@ -28,6 +29,7 @@ #: Python 3.x? is_py3 = (_ver[0] == 3) +# json/simplejson module import resolution has_simplejson = False try: import simplejson as json @@ -35,47 +37,27 @@ except ImportError: import json +if has_simplejson: + from simplejson import JSONDecodeError +else: + from json import JSONDecodeError + # --------- -# Specifics +# Legacy Imports # --------- - -if is_py2: - from urllib import ( - quote, unquote, quote_plus, unquote_plus, urlencode, getproxies, - proxy_bypass, proxy_bypass_environment, getproxies_environment) - from urlparse import urlparse, urlunparse, urljoin, urlsplit, urldefrag - from urllib2 import parse_http_list - import cookielib - from Cookie import Morsel - from StringIO import StringIO - # Keep OrderedDict for backwards compatibility. - from collections import Callable, Mapping, MutableMapping, OrderedDict - - builtin_str = str - bytes = str - str = unicode - basestring = basestring - numeric_types = (int, long, float) - integer_types = (int, long) - JSONDecodeError = ValueError - -elif is_py3: - from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag - from urllib.request import parse_http_list, getproxies, proxy_bypass, proxy_bypass_environment, getproxies_environment - from http import cookiejar as cookielib - from http.cookies import Morsel - from io import StringIO - # Keep OrderedDict for backwards compatibility. - from collections import OrderedDict - from collections.abc import Callable, Mapping, MutableMapping - if has_simplejson: - from simplejson import JSONDecodeError - else: - from json import JSONDecodeError - - builtin_str = str - str = str - bytes = bytes - basestring = (str, bytes) - numeric_types = (int, float) - integer_types = (int,) +from urllib.parse import urlparse, urlunparse, urljoin, urlsplit, urlencode, quote, unquote, quote_plus, unquote_plus, urldefrag +from urllib.request import parse_http_list, getproxies, proxy_bypass, proxy_bypass_environment, getproxies_environment +from http import cookiejar as cookielib +from http.cookies import Morsel +from io import StringIO + +# Keep OrderedDict for backwards compatibility. +from collections import OrderedDict +from collections.abc import Callable, Mapping, MutableMapping + +builtin_str = str +str = str +bytes = bytes +basestring = (str, bytes) +numeric_types = (int, float) +integer_types = (int,) diff --git a/requests/help.py b/requests/help.py index 4cd6389f55..753633f944 100644 --- a/requests/help.py +++ b/requests/help.py @@ -36,8 +36,8 @@ def _implementation(): """Return a dict with the Python implementation and version. Provide both the name and the version of the Python implementation - currently running. For example, on CPython 2.7.5 it will return - {'name': 'CPython', 'version': '2.7.5'}. + currently running. For example, on CPython 3.10.3 it will return + {'name': 'CPython', 'version': '3.10.3'}. This function works best on CPython and PyPy: in particular, it probably doesn't work for Jython or IronPython. Future investigation should be done diff --git a/requests/models.py b/requests/models.py index f65afa858f..539879feb3 100644 --- a/requests/models.py +++ b/requests/models.py @@ -8,7 +8,6 @@ """ import datetime -import sys # Import encoding now, to avoid implicit import later. # Implicit import within threads may cause LookupError when standard library is in a ZIP, @@ -45,8 +44,8 @@ iter_slices, guess_json_utf, super_len, check_header_validity) from .compat import ( Callable, Mapping, - cookielib, urlunparse, urlsplit, urlencode, str, bytes, - is_py2, chardet, builtin_str, basestring, JSONDecodeError) + cookielib, urlunparse, urlsplit, urlencode, + chardet, builtin_str, basestring, JSONDecodeError) from .compat import json as complexjson from .status_codes import codes @@ -373,7 +372,7 @@ def prepare_url(self, url, params): if isinstance(url, bytes): url = url.decode('utf8') else: - url = unicode(url) if is_py2 else str(url) + url = str(url) # Remove leading whitespaces from url url = url.lstrip() @@ -424,18 +423,6 @@ def prepare_url(self, url, params): if not path: path = '/' - if is_py2: - if isinstance(scheme, str): - scheme = scheme.encode('utf-8') - if isinstance(netloc, str): - netloc = netloc.encode('utf-8') - if isinstance(path, str): - path = path.encode('utf-8') - if isinstance(query, str): - query = query.encode('utf-8') - if isinstance(fragment, str): - fragment = fragment.encode('utf-8') - if isinstance(params, (str, bytes)): params = to_native_string(params) @@ -919,10 +906,7 @@ def json(self, **kwargs): except JSONDecodeError as e: # Catch JSON-related errors and raise as requests.JSONDecodeError # This aliases json.JSONDecodeError and simplejson.JSONDecodeError - if is_py2: # e is a ValueError - raise RequestsJSONDecodeError(e.message) - else: - raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) + raise RequestsJSONDecodeError(e.msg, e.doc, e.pos) @property def links(self): diff --git a/requests/sessions.py b/requests/sessions.py index d72ae4674a..f2d7d3e36f 100644 --- a/requests/sessions.py +++ b/requests/sessions.py @@ -14,7 +14,7 @@ from collections import OrderedDict from .auth import _basic_auth_str -from .compat import cookielib, is_py3, urljoin, urlparse, Mapping +from .compat import cookielib, urljoin, urlparse, Mapping from .cookies import ( cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies) from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT @@ -39,10 +39,7 @@ # Preferred clock, based on which one is more accurate on a given system. if sys.platform == 'win32': - try: # Python 3.4+ - preferred_clock = time.perf_counter - except AttributeError: # Earlier than Python 3. - preferred_clock = time.clock + preferred_clock = time.perf_counter else: preferred_clock = time.time @@ -111,8 +108,7 @@ def get_redirect_target(self, resp): # It is more likely to get UTF8 header rather than latin1. # This causes incorrect handling of UTF8 encoded location headers. # To solve this, we re-encode the location in latin1. - if is_py3: - location = location.encode('latin1') + location = location.encode('latin1') return to_native_string(location, 'utf8') return None diff --git a/requests/utils.py b/requests/utils.py index 153776c7f3..a58b26a0c2 100644 --- a/requests/utils.py +++ b/requests/utils.py @@ -30,7 +30,7 @@ from .compat import parse_http_list as _parse_list_header from .compat import ( quote, urlparse, bytes, str, unquote, getproxies, - proxy_bypass, urlunparse, basestring, integer_types, is_py3, + proxy_bypass, urlunparse, basestring, integer_types, proxy_bypass_environment, getproxies_environment, Mapping) from .cookies import cookiejar_from_dict from .structures import CaseInsensitiveDict @@ -54,10 +54,7 @@ def proxy_bypass_registry(host): try: - if is_py3: - import winreg - else: - import _winreg as winreg + import winreg except ImportError: return False @@ -281,12 +278,11 @@ def extract_zipped_paths(path): @contextlib.contextmanager def atomic_open(filename): """Write a file to the disk in an atomic fashion""" - replacer = os.rename if sys.version_info[0] == 2 else os.replace tmp_descriptor, tmp_name = tempfile.mkstemp(dir=os.path.dirname(filename)) try: with os.fdopen(tmp_descriptor, 'wb') as tmp_handler: yield tmp_handler - replacer(tmp_name, filename) + os.replace(tmp_name, filename) except BaseException: os.remove(tmp_name) raise diff --git a/setup.cfg b/setup.cfg index ed8a958e0a..fa7fc96a30 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,10 @@ -[bdist_wheel] -universal = 1 - [metadata] license_file = LICENSE +provides-extra = + socks + use_chardet_on_py3 +requires-dist = + certifi>=2017.4.17 + charset_normalizer~=2.0.0 + idna>=2.5,<4 + urllib3>=1.21.1,<1.27 diff --git a/setup.py b/setup.py index c279c81ba9..7a27fc2839 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# Learn more: https://github.com/kennethreitz/setup.py import os import sys @@ -38,16 +37,11 @@ def run_tests(self): os.system('twine upload dist/*') sys.exit() -packages = ['requests'] - requires = [ - 'charset_normalizer~=2.0.0; python_version >= "3"', - 'chardet>=3.0.2,<5; python_version < "3"', - 'idna>=2.5,<3; python_version < "3"', - 'idna>=2.5,<4; python_version >= "3"', + 'charset_normalizer~=2.0.0', + 'idna>=2.5,<4', 'urllib3>=1.21.1,<1.27', - 'certifi>=2017.4.17' - + 'certifi>=2017.4.17', ] test_requirements = [ 'pytest-httpbin==0.0.7', @@ -55,7 +49,7 @@ def run_tests(self): 'pytest-mock', 'pytest-xdist', 'PySocks>=1.5.6, !=1.5.7', - 'pytest>=3' + 'pytest>=3', ] about = {} @@ -74,11 +68,11 @@ def run_tests(self): author=about['__author__'], author_email=about['__author_email__'], url=about['__url__'], - packages=packages, + packages=['requests'], package_data={'': ['LICENSE', 'NOTICE']}, package_dir={'requests': 'requests'}, include_package_data=True, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*", + python_requires=">=3.7, <4", install_requires=requires, license=about['__license__'], zip_safe=False, @@ -90,14 +84,13 @@ def run_tests(self): 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet :: WWW/HTTP', @@ -108,7 +101,6 @@ def run_tests(self): extras_require={ 'security': [], 'socks': ['PySocks>=1.5.6, !=1.5.7'], - 'socks:sys_platform == "win32" and python_version == "2.7"': ['win_inet_pton'], 'use_chardet_on_py3': ['chardet>=3.0.2,<5'] }, project_urls={ diff --git a/tests/compat.py b/tests/compat.py index f68e801444..62abb25dca 100644 --- a/tests/compat.py +++ b/tests/compat.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- - -from requests.compat import is_py3 - +import warnings try: import StringIO @@ -13,9 +11,14 @@ except ImportError: cStringIO = None -if is_py3: - def u(s): - return s -else: - def u(s): - return s.decode('unicode-escape') + +def u(s): + warnings.warn( + ( + "This helper function is no longer relevant in Python 3. " + "Usage of this alias should be discontinued as it will be " + "removed in a future release of Requests." + ), + DeprecationWarning, + ) + return s diff --git a/tests/test_requests.py b/tests/test_requests.py index 19e483d3f1..e69232e9f0 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -2,7 +2,6 @@ """Tests for Requests.""" -from __future__ import division import json import os import pickle @@ -18,7 +17,7 @@ from requests.adapters import HTTPAdapter from requests.auth import HTTPDigestAuth, _basic_auth_str from requests.compat import ( - Morsel, cookielib, getproxies, str, urlparse, + Morsel, cookielib, getproxies, urlparse, builtin_str) from requests.cookies import ( cookiejar_from_dict, morsel_to_cookie) @@ -47,9 +46,9 @@ from requests.sessions import SessionRedirectMixin from requests.models import urlencode from requests.hooks import default_hooks -from requests.compat import JSONDecodeError, is_py3, MutableMapping +from requests.compat import JSONDecodeError, MutableMapping -from .compat import StringIO, u +from .compat import StringIO from .utils import override_environ from urllib3.util import Timeout as Urllib3Timeout @@ -844,7 +843,7 @@ def test_conflicting_post_params(self, httpbin): with pytest.raises(ValueError): requests.post(url, data='[{"some": "data"}]', files={'some': f}) with pytest.raises(ValueError): - requests.post(url, data=u('[{"some": "data"}]'), files={'some': f}) + requests.post(url, data=u'[{"some": "data"}]', files={'some': f}) def test_request_ok_set(self, httpbin): r = requests.get(httpbin('status', '404')) @@ -989,8 +988,8 @@ def test_different_encodings_dont_break_post(self, httpbin): @pytest.mark.parametrize( 'data', ( - {'stuff': u('ëlïxr')}, - {'stuff': u('ëlïxr').encode('utf-8')}, + {'stuff': u'ëlïxr'}, + {'stuff': u'ëlïxr'.encode('utf-8')}, {'stuff': 'elixr'}, {'stuff': 'elixr'.encode('utf-8')}, )) @@ -1013,13 +1012,13 @@ def test_unicode_multipart_post_fieldnames(self, httpbin): def test_unicode_method_name(self, httpbin): files = {'file': open(__file__, 'rb')} r = requests.request( - method=u('POST'), url=httpbin('post'), files=files) + method=u'POST', url=httpbin('post'), files=files) assert r.status_code == 200 def test_unicode_method_name_with_request_object(self, httpbin): files = {'file': open(__file__, 'rb')} s = requests.Session() - req = requests.Request(u('POST'), httpbin('post'), files=files) + req = requests.Request(u'POST', httpbin('post'), files=files) prep = s.prepare_request(req) assert isinstance(prep.method, builtin_str) assert prep.method == 'POST' @@ -1029,7 +1028,7 @@ def test_unicode_method_name_with_request_object(self, httpbin): def test_non_prepared_request_error(self): s = requests.Session() - req = requests.Request(u('POST'), '/') + req = requests.Request(u'POST', '/') with pytest.raises(ValueError) as e: s.send(req) @@ -1593,7 +1592,7 @@ def test_long_authinfo_in_url(self): assert r.url == url def test_header_keys_are_native(self, httpbin): - headers = {u('unicode'): 'blah', 'byte'.encode('ascii'): 'blah'} + headers = {u'unicode': 'blah', 'byte'.encode('ascii'): 'blah'} r = requests.Request('GET', httpbin('get'), headers=headers) p = r.prepare() @@ -2469,7 +2468,7 @@ def test_data_argument_accepts_tuples(data): }, { 'method': 'GET', - 'url': u('http://www.example.com/üniçø∂é') + 'url': u'http://www.example.com/üniçø∂é' }, )) def test_prepared_copy(kwargs): @@ -2664,7 +2663,6 @@ def test_json_decode_compatibility(self, httpbin): assert isinstance(excinfo.value, JSONDecodeError) assert r.text not in str(excinfo.value) - @pytest.mark.skipif(not is_py3, reason="doc attribute is only present on py3") def test_json_decode_persists_doc_attr(self, httpbin): r = requests.get(httpbin('bytes/20')) with pytest.raises(requests.exceptions.JSONDecodeError) as excinfo: diff --git a/tests/test_utils.py b/tests/test_utils.py index 8e98397204..931a1b92c7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -749,10 +749,7 @@ def test_should_bypass_proxies_win_registry(url, expected, override, """ if override is None: override = '192.168.*;127.0.0.1;localhost.localdomain;172.16.1.1' - if compat.is_py3: - import winreg - else: - import _winreg as winreg + import winreg class RegHandle: def Close(self): diff --git a/tests/testserver/server.py b/tests/testserver/server.py index c4587eefba..92dcb6cb1a 100644 --- a/tests/testserver/server.py +++ b/tests/testserver/server.py @@ -78,9 +78,7 @@ def run(self): def _create_socket_and_bind(self): sock = socket.socket() sock.bind((self.host, self.port)) - # NB: when Python 2.7 is no longer supported, the argument - # can be removed to use a default backlog size - sock.listen(5) + sock.listen() return sock def _close_server_sock_ignore_errors(self): diff --git a/tox.ini b/tox.ini index 5e3d53774e..2e4fd90d32 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{27,36,37,38,39}-{default,use_chardet_on_py3} +envlist = py{37,38,39,310}-{default, use_chardet_on_py3} [testenv] deps = -rrequirements-dev.txt