未验证 提交 8c5a41ba 编写于 作者: J Jon Dufresne 提交者: GitHub

Remove support for end-of-life Python 2.7 (#1318)

Remove support for end-of-life Python 2.7

Python 2.7 is end of life. It is no longer receiving bug fixes,
including for security issues. Python 2.7 went EOL on 2020-01-01. For
additional details on support Python versions, see:

Supported: https://devguide.python.org/#status-of-python-branches
EOL: https://devguide.python.org/devcycle/#end-of-life-branches

Removing support for EOL Pythons will reduce testing and maintenance
resources while allowing the library to move towards a modern Python 3
style. Python 2.7 users can continue to use the previous version of
redis-py.

Was able to simplify the code:

- Removed redis._compat module
- Removed __future__ imports
- Removed object from class definition (all classes are new style)
- Removed long (Python 3 unified numeric types)
- Removed deprecated __nonzero__ method
- Use simpler Python 3 super() syntax
- Use unified OSError exception
- Use yield from syntax
Co-authored-by: NAndy McCurdy <andy@andymccurdy.com>
上级 c6f13c3b
**/__pycache__
**/*.pyc
.tox
.coverage
.coverage.*
* (in development)
* Removed support for end of life Python 2.7.
* Provide a development and testing environment via docker. Thanks
@abrookins. #1365
* Added support for the LPOS command available in Redis 6.0.6. Thanks
......
......@@ -87,7 +87,7 @@ provide an upgrade path for users migrating from 2.X to 3.0.
Python Version Support
^^^^^^^^^^^^^^^^^^^^^^
redis-py 3.0 supports Python 2.7 and Python 3.5+.
redis-py supports Python 3.5+.
Client Classes: Redis and StrictRedis
......
......@@ -3,10 +3,9 @@ import itertools
import redis
import sys
import timeit
from redis._compat import izip
class Benchmark(object):
class Benchmark:
ARGUMENTS = ()
def __init__(self):
......@@ -34,7 +33,7 @@ class Benchmark(object):
group_names = [group['name'] for group in self.ARGUMENTS]
group_values = [group['values'] for group in self.ARGUMENTS]
for value_set in itertools.product(*group_values):
pairs = list(izip(group_names, value_set))
pairs = list(zip(group_names, value_set))
arg_string = ', '.join(['%s=%s' % (p[0], p[1]) for p in pairs])
sys.stdout.write('Benchmark: %s... ' % arg_string)
sys.stdout.flush()
......
from __future__ import print_function
import redis
import time
import sys
from functools import wraps
from argparse import ArgumentParser
if sys.version_info[0] == 3:
long = int
def parse_args():
parser = ArgumentParser()
......@@ -47,9 +42,9 @@ def run():
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.clock()
start = time.monotonic()
ret = func(*args, **kwargs)
duration = time.clock() - start
duration = time.monotonic() - start
if 'num' in kwargs:
count = kwargs['num']
else:
......@@ -57,7 +52,7 @@ def timer(func):
print('{} - {} Requests'.format(func.__name__, count))
print('Duration = {}'.format(duration))
print('Rate = {}'.format(count/duration))
print('')
print()
return ret
return wrapper
......@@ -185,7 +180,6 @@ def hmset(conn, num, pipeline_size, data_size):
set_data = {'str_value': 'string',
'int_value': 123456,
'long_value': long(123456),
'float_value': 123456.0}
for i in range(num):
conn.hmset('hmset_key', set_data)
......
import socket
from redis.connection import (Connection, SYM_STAR, SYM_DOLLAR, SYM_EMPTY,
SYM_CRLF)
from redis._compat import imap
from base import Benchmark
......@@ -29,7 +28,7 @@ class StringJoiningConnection(Connection):
args_output = SYM_EMPTY.join([
SYM_EMPTY.join(
(SYM_DOLLAR, str(len(k)).encode(), SYM_CRLF, k, SYM_CRLF))
for k in imap(self.encoder.encode, args)])
for k in map(self.encoder.encode, args)])
output = SYM_EMPTY.join(
(SYM_STAR, str(len(args)).encode(), SYM_CRLF, args_output))
return output
......@@ -61,7 +60,7 @@ class ListJoiningConnection(Connection):
buff = SYM_EMPTY.join(
(SYM_STAR, str(len(args)).encode(), SYM_CRLF))
for k in imap(self.encoder.encode, args):
for k in map(self.encoder.encode, args):
if len(buff) > 6000 or len(k) > 6000:
buff = SYM_EMPTY.join(
(buff, SYM_DOLLAR, str(len(k)).encode(), SYM_CRLF))
......
# -*- coding: utf-8 -*-
#
# redis-py documentation build configuration file, created by
# sphinx-quickstart on Fri Feb 8 00:47:08 2013.
#
......@@ -43,8 +41,8 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
project = u'redis-py'
copyright = u'2016, Andy McCurdy'
project = 'redis-py'
copyright = '2016, Andy McCurdy'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
......@@ -188,8 +186,8 @@ latex_elements = {
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index', 'redis-py.tex', u'redis-py Documentation',
u'Andy McCurdy', 'manual'),
('index', 'redis-py.tex', 'redis-py Documentation',
'Andy McCurdy', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
......@@ -218,8 +216,8 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'redis-py', u'redis-py Documentation',
[u'Andy McCurdy'], 1)
('index', 'redis-py', 'redis-py Documentation',
['Andy McCurdy'], 1)
]
# If true, show URL addresses after external links.
......@@ -232,8 +230,8 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'redis-py', u'redis-py Documentation',
u'Andy McCurdy', 'redis-py',
('index', 'redis-py', 'redis-py Documentation',
'Andy McCurdy', 'redis-py',
'One line description of project.', 'Miscellaneous'),
]
......@@ -246,7 +244,7 @@ texinfo_documents = [
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
epub_title = u'redis-py'
epub_author = u'Andy McCurdy'
epub_publisher = u'Andy McCurdy'
epub_copyright = u'2011, Andy McCurdy'
epub_title = 'redis-py'
epub_author = 'Andy McCurdy'
epub_publisher = 'Andy McCurdy'
epub_copyright = '2011, Andy McCurdy'
"""Internal module for Python 2 backwards compatibility."""
# flake8: noqa
import errno
import socket
import sys
def sendall(sock, *args, **kwargs):
return sock.sendall(*args, **kwargs)
def shutdown(sock, *args, **kwargs):
return sock.shutdown(*args, **kwargs)
def ssl_wrap_socket(context, sock, *args, **kwargs):
return context.wrap_socket(sock, *args, **kwargs)
# For Python older than 3.5, retry EINTR.
if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and
sys.version_info[1] < 5):
# Adapted from https://bugs.python.org/review/23863/patch/14532/54418
import time
# Wrapper for handling interruptable system calls.
def _retryable_call(s, func, *args, **kwargs):
# Some modules (SSL) use the _fileobject wrapper directly and
# implement a smaller portion of the socket interface, thus we
# need to let them continue to do so.
timeout, deadline = None, 0.0
attempted = False
try:
timeout = s.gettimeout()
except AttributeError:
pass
if timeout:
deadline = time.time() + timeout
try:
while True:
if attempted and timeout:
now = time.time()
if now >= deadline:
raise socket.error(errno.EWOULDBLOCK, "timed out")
else:
# Overwrite the timeout on the socket object
# to take into account elapsed time.
s.settimeout(deadline - now)
try:
attempted = True
return func(*args, **kwargs)
except socket.error as e:
if e.args[0] == errno.EINTR:
continue
raise
finally:
# Set the existing timeout back for future
# calls.
if timeout:
s.settimeout(timeout)
def recv(sock, *args, **kwargs):
return _retryable_call(sock, sock.recv, *args, **kwargs)
def recv_into(sock, *args, **kwargs):
return _retryable_call(sock, sock.recv_into, *args, **kwargs)
else: # Python 3.5 and above automatically retry EINTR
def recv(sock, *args, **kwargs):
return sock.recv(*args, **kwargs)
def recv_into(sock, *args, **kwargs):
return sock.recv_into(*args, **kwargs)
if sys.version_info[0] < 3:
# In Python 3, the ssl module raises socket.timeout whereas it raises
# SSLError in Python 2. For compatibility between versions, ensure
# socket.timeout is raised for both.
import functools
try:
from ssl import SSLError as _SSLError
except ImportError:
class _SSLError(Exception):
"""A replacement in case ssl.SSLError is not available."""
pass
_EXPECTED_SSL_TIMEOUT_MESSAGES = (
"The handshake operation timed out",
"The read operation timed out",
"The write operation timed out",
)
def _handle_ssl_timeout(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except _SSLError as e:
message = len(e.args) == 1 and unicode(e.args[0]) or ''
if any(x in message for x in _EXPECTED_SSL_TIMEOUT_MESSAGES):
# Raise socket.timeout for compatibility with Python 3.
raise socket.timeout(*e.args)
raise
return wrapper
recv = _handle_ssl_timeout(recv)
recv_into = _handle_ssl_timeout(recv_into)
sendall = _handle_ssl_timeout(sendall)
shutdown = _handle_ssl_timeout(shutdown)
ssl_wrap_socket = _handle_ssl_timeout(ssl_wrap_socket)
if sys.version_info[0] < 3:
from urllib import unquote
from urlparse import parse_qs, urlparse
from itertools import imap, izip
from string import letters as ascii_letters
from Queue import Queue
# special unicode handling for python2 to avoid UnicodeDecodeError
def safe_unicode(obj, *args):
""" return the unicode representation of obj """
try:
return unicode(obj, *args)
except UnicodeDecodeError:
# obj is byte string
ascii_text = str(obj).encode('string_escape')
return unicode(ascii_text)
def iteritems(x):
return x.iteritems()
def iterkeys(x):
return x.iterkeys()
def itervalues(x):
return x.itervalues()
def nativestr(x):
return x if isinstance(x, str) else x.encode('utf-8', 'replace')
def next(x):
return x.next()
unichr = unichr
xrange = xrange
basestring = basestring
unicode = unicode
long = long
BlockingIOError = socket.error
else:
from urllib.parse import parse_qs, unquote, urlparse
from string import ascii_letters
from queue import Queue
def iteritems(x):
return iter(x.items())
def iterkeys(x):
return iter(x.keys())
def itervalues(x):
return iter(x.values())
def nativestr(x):
return x if isinstance(x, str) else x.decode('utf-8', 'replace')
def safe_unicode(value):
if isinstance(value, bytes):
value = value.decode('utf-8', 'replace')
return str(value)
next = next
unichr = chr
imap = map
izip = zip
xrange = range
basestring = str
unicode = str
long = int
BlockingIOError = BlockingIOError
try: # Python 3
from queue import LifoQueue, Empty, Full
except ImportError: # Python 2
from Queue import LifoQueue, Empty, Full
此差异已折叠。
from __future__ import unicode_literals
from distutils.version import StrictVersion
from itertools import chain
from time import time
from queue import LifoQueue, Empty, Full
from urllib.parse import parse_qs, unquote, urlparse
import errno
import io
import os
......@@ -9,11 +10,6 @@ import socket
import threading
import warnings
from redis._compat import (xrange, imap, unicode, long,
nativestr, basestring, iteritems,
LifoQueue, Empty, Full, urlparse, parse_qs,
recv, recv_into, unquote, BlockingIOError,
sendall, shutdown, ssl_wrap_socket)
from redis.exceptions import (
AuthenticationError,
AuthenticationWrongNumberOfArgsError,
......@@ -31,7 +27,7 @@ from redis.exceptions import (
TimeoutError,
ModuleError,
)
from redis.utils import HIREDIS_AVAILABLE
from redis.utils import HIREDIS_AVAILABLE, str_if_bytes
try:
import ssl
......@@ -50,16 +46,6 @@ if ssl_available:
else:
NONBLOCKING_EXCEPTION_ERROR_NUMBERS[ssl.SSLError] = 2
# In Python 2.7 a socket.error is raised for a nonblocking read.
# The _compat module aliases BlockingIOError to socket.error to be
# Python 2/3 compatible.
# However this means that all socket.error exceptions need to be handled
# properly within these exception handlers.
# We need to make sure socket.error is included in these handlers and
# provide a dummy error number that will never match a real exception.
if socket.error not in NONBLOCKING_EXCEPTION_ERROR_NUMBERS:
NONBLOCKING_EXCEPTION_ERROR_NUMBERS[socket.error] = -999999
NONBLOCKING_EXCEPTIONS = tuple(NONBLOCKING_EXCEPTION_ERROR_NUMBERS.keys())
if HIREDIS_AVAILABLE:
......@@ -101,7 +87,7 @@ MODULE_EXPORTS_DATA_TYPES_ERROR = "Error unloading module: the module " \
"types, can't unload"
class Encoder(object):
class Encoder:
"Encode strings to bytes-like and decode bytes-like to strings"
def __init__(self, encoding, encoding_errors, decode_responses):
......@@ -117,17 +103,14 @@ class Encoder(object):
# special case bool since it is a subclass of int
raise DataError("Invalid input of type: 'bool'. Convert to a "
"bytes, string, int or float first.")
elif isinstance(value, float):
elif isinstance(value, (int, float)):
value = repr(value).encode()
elif isinstance(value, (int, long)):
# python 2 repr() on longs is '123L', so use str() instead
value = str(value).encode()
elif not isinstance(value, basestring):
elif not isinstance(value, str):
# a value we don't know how to deal with. throw an error
typename = type(value).__name__
raise DataError("Invalid input of type: '%s'. Convert to a "
"bytes, string, int or float first." % typename)
if isinstance(value, unicode):
if isinstance(value, str):
value = value.encode(self.encoding, self.encoding_errors)
return value
......@@ -141,7 +124,7 @@ class Encoder(object):
return value
class BaseParser(object):
class BaseParser:
EXCEPTION_CLASSES = {
'ERR': {
'max number of clients reached': ConnectionError,
......@@ -180,7 +163,7 @@ class BaseParser(object):
return ResponseError(response)
class SocketBuffer(object):
class SocketBuffer:
def __init__(self, socket, socket_read_size, socket_timeout):
self._sock = socket
self.socket_read_size = socket_read_size
......@@ -208,7 +191,7 @@ class SocketBuffer(object):
if custom_timeout:
sock.settimeout(timeout)
while True:
data = recv(self._sock, socket_read_size)
data = self._sock.recv(socket_read_size)
# an empty string indicates the server shutdown the socket
if isinstance(data, bytes) and len(data) == 0:
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
......@@ -345,7 +328,7 @@ class PythonParser(BaseParser):
# server returned an error
if byte == b'-':
response = nativestr(response)
response = response.decode('utf-8', errors='replace')
error = self.parse_error(response)
# if the error is a ConnectionError, raise immediately so the user
# is notified
......@@ -361,7 +344,7 @@ class PythonParser(BaseParser):
pass
# int value
elif byte == b':':
response = long(response)
response = int(response)
# bulk response
elif byte == b'$':
length = int(response)
......@@ -373,7 +356,7 @@ class PythonParser(BaseParser):
length = int(response)
if length == -1:
return None
response = [self.read_response() for i in xrange(length)]
response = [self.read_response() for i in range(length)]
if isinstance(response, bytes):
response = self.encoder.decode(response)
return response
......@@ -437,12 +420,12 @@ class HiredisParser(BaseParser):
if custom_timeout:
sock.settimeout(timeout)
if HIREDIS_USE_BYTE_BUFFER:
bufflen = recv_into(self._sock, self._buffer)
bufflen = self._sock.recv_into(self._buffer)
if bufflen == 0:
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
self._reader.feed(self._buffer, 0, bufflen)
else:
buffer = recv(self._sock, self.socket_read_size)
buffer = self._sock.recv(self.socket_read_size)
# an empty string indicates the server shutdown the socket
if not isinstance(buffer, bytes) or len(buffer) == 0:
raise ConnectionError(SERVER_CLOSED_CONNECTION_ERROR)
......@@ -507,7 +490,7 @@ else:
DefaultParser = PythonParser
class Connection(object):
class Connection:
"Manages TCP communication to and from a Redis server"
def __init__(self, host='localhost', port=6379, db=0, password=None,
......@@ -606,7 +589,7 @@ class Connection(object):
# TCP_KEEPALIVE
if self.socket_keepalive:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
for k, v in iteritems(self.socket_keepalive_options):
for k, v in self.socket_keepalive_options.items():
sock.setsockopt(socket.IPPROTO_TCP, k, v)
# set the socket_connect_timeout before we connect
......@@ -619,14 +602,14 @@ class Connection(object):
sock.settimeout(self.socket_timeout)
return sock
except socket.error as _:
except OSError as _:
err = _
if sock is not None:
sock.close()
if err is not None:
raise err
raise socket.error("socket.getaddrinfo returned an empty list")
raise OSError("socket.getaddrinfo returned an empty list")
def _error_message(self, exception):
# args for socket.error can either be (errno, "message")
......@@ -662,19 +645,19 @@ class Connection(object):
self.send_command('AUTH', self.password, check_health=False)
auth_response = self.read_response()
if nativestr(auth_response) != 'OK':
if str_if_bytes(auth_response) != 'OK':
raise AuthenticationError('Invalid Username or Password')
# if a client_name is given, set it
if self.client_name:
self.send_command('CLIENT', 'SETNAME', self.client_name)
if nativestr(self.read_response()) != 'OK':
if str_if_bytes(self.read_response()) != 'OK':
raise ConnectionError('Error setting client name')
# if a database is specified, switch to it
if self.db:
self.send_command('SELECT', self.db)
if nativestr(self.read_response()) != 'OK':
if str_if_bytes(self.read_response()) != 'OK':
raise ConnectionError('Invalid Database')
def disconnect(self):
......@@ -684,9 +667,9 @@ class Connection(object):
return
try:
if os.getpid() == self.pid:
shutdown(self._sock, socket.SHUT_RDWR)
self._sock.shutdown(socket.SHUT_RDWR)
self._sock.close()
except socket.error:
except OSError:
pass
self._sock = None
......@@ -695,13 +678,13 @@ class Connection(object):
if self.health_check_interval and time() > self.next_health_check:
try:
self.send_command('PING', check_health=False)
if nativestr(self.read_response()) != 'PONG':
if str_if_bytes(self.read_response()) != 'PONG':
raise ConnectionError(
'Bad response from PING health check')
except (ConnectionError, TimeoutError):
self.disconnect()
self.send_command('PING', check_health=False)
if nativestr(self.read_response()) != 'PONG':
if str_if_bytes(self.read_response()) != 'PONG':
raise ConnectionError(
'Bad response from PING health check')
......@@ -716,7 +699,7 @@ class Connection(object):
if isinstance(command, str):
command = [command]
for item in command:
sendall(self._sock, item)
self._sock.sendall(item)
except socket.timeout:
self.disconnect()
raise TimeoutError("Timeout writing to socket")
......@@ -777,7 +760,7 @@ class Connection(object):
# arguments to be sent separately, so split the first argument
# manually. These arguments should be bytestrings so that they are
# not encoded.
if isinstance(args[0], unicode):
if isinstance(args[0], str):
args = tuple(args[0].encode().split()) + args[1:]
elif b' ' in args[0]:
args = tuple(args[0].split()) + args[1:]
......@@ -785,7 +768,7 @@ class Connection(object):
buff = SYM_EMPTY.join((SYM_STAR, str(len(args)).encode(), SYM_CRLF))
buffer_cutoff = self._buffer_cutoff
for arg in imap(self.encoder.encode, args):
for arg in map(self.encoder.encode, args):
# to avoid large string mallocs, chunk the command into the
# output list if we're sending large values or memoryviews
arg_length = len(arg)
......@@ -838,13 +821,13 @@ class SSLConnection(Connection):
if not ssl_available:
raise RedisError("Python wasn't built with SSL support")
super(SSLConnection, self).__init__(**kwargs)
super().__init__(**kwargs)
self.keyfile = ssl_keyfile
self.certfile = ssl_certfile
if ssl_cert_reqs is None:
ssl_cert_reqs = ssl.CERT_NONE
elif isinstance(ssl_cert_reqs, basestring):
elif isinstance(ssl_cert_reqs, str):
CERT_REQS = {
'none': ssl.CERT_NONE,
'optional': ssl.CERT_OPTIONAL,
......@@ -861,27 +844,16 @@ class SSLConnection(Connection):
def _connect(self):
"Wrap the socket with SSL support"
sock = super(SSLConnection, self)._connect()
if hasattr(ssl, "create_default_context"):
context = ssl.create_default_context()
context.check_hostname = self.check_hostname
context.verify_mode = self.cert_reqs
if self.certfile and self.keyfile:
context.load_cert_chain(certfile=self.certfile,
keyfile=self.keyfile)
if self