Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove support for Python 2.7, 3.6 and Pypy3.6 #6091

Merged
merged 3 commits into from Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/run-tests.yml
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions HISTORY.md
Expand Up @@ -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)
-------------------

Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -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

Expand Down
15 changes: 6 additions & 9 deletions docs/community/faq.rst
Expand Up @@ -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 <https://github.com/psf/requests/issues/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?
Expand All @@ -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
2 changes: 1 addition & 1 deletion docs/index.rst
Expand Up @@ -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
Expand Down
7 changes: 2 additions & 5 deletions requests/_internal_utils.py
Expand Up @@ -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'):
Expand All @@ -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

Expand Down
7 changes: 1 addition & 6 deletions requests/adapters.py
Expand Up @@ -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,
Expand Down
70 changes: 26 additions & 44 deletions requests/compat.py
Expand Up @@ -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:
Expand All @@ -28,54 +29,35 @@
#: Python 3.x?
is_py3 = (_ver[0] == 3)

# json/simplejson module import resolution
has_simplejson = False
try:
import simplejson as json
has_simplejson = True
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,)
4 changes: 2 additions & 2 deletions requests/help.py
Expand Up @@ -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
Expand Down
24 changes: 4 additions & 20 deletions requests/models.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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):
Expand Down
10 changes: 3 additions & 7 deletions requests/sessions.py
Expand Up @@ -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
Expand All @@ -39,10 +39,7 @@

# Preferred clock, based on which one is more accurate on a given system.
if sys.platform == 'win32':
nateprewitt marked this conversation as resolved.
Show resolved Hide resolved
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

Expand Down Expand Up @@ -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

Expand Down
10 changes: 3 additions & 7 deletions requests/utils.py
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions 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