Update cheroot-8.5.2

This commit is contained in:
JonnyWong16 2021-10-14 21:14:02 -07:00
parent 4ac151d7de
commit 182e5f553e
No known key found for this signature in database
GPG key ID: B1F1F9807184697A
25 changed files with 2171 additions and 602 deletions

View file

@ -7,12 +7,13 @@ sticking incoming connections onto a Queue::
server = HTTPServer(...)
server.start()
-> while True:
tick()
# This blocks until a request comes in:
child = socket.accept()
conn = HTTPConnection(child, ...)
server.requests.put(conn)
-> serve()
while ready:
_connections.run()
while not stop_requested:
child = socket.accept() # blocks until a request comes in
conn = HTTPConnection(child, ...)
server.process_conn(conn) # adds conn to threadpool
Worker threads are kept in a pool and poll the Queue, popping off and then
handling each connection in turn. Each connection can consist of an arbitrary
@ -58,7 +59,6 @@ And now for a trivial doctest to exercise the test suite
>>> 'HTTPServer' in globals()
True
"""
from __future__ import absolute_import, division, print_function
@ -74,6 +74,8 @@ import time
import traceback as traceback_
import logging
import platform
import contextlib
import threading
try:
from functools import lru_cache
@ -93,6 +95,7 @@ from .makefile import MakeFile, StreamWriter
__all__ = (
'HTTPRequest', 'HTTPConnection', 'HTTPServer',
'HeaderReader', 'DropUnderscoreHeaderReader',
'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile',
'Gateway', 'get_ssl_adapter_class',
)
@ -156,7 +159,10 @@ EMPTY = b''
ASTERISK = b'*'
FORWARD_SLASH = b'/'
QUOTED_SLASH = b'%2F'
QUOTED_SLASH_REGEX = re.compile(b'(?i)' + QUOTED_SLASH)
QUOTED_SLASH_REGEX = re.compile(b''.join((b'(?i)', QUOTED_SLASH)))
_STOPPING_FOR_INTERRUPT = object() # sentinel used during shutdown
comma_separated_headers = [
@ -179,7 +185,7 @@ class HeaderReader:
Interface and default implementation.
"""
def __call__(self, rfile, hdict=None):
def __call__(self, rfile, hdict=None): # noqa: C901 # FIXME
"""
Read headers from the given stream into the given header dict.
@ -248,15 +254,14 @@ class DropUnderscoreHeaderReader(HeaderReader):
class SizeCheckWrapper:
"""Wraps a file-like object, raising MaxSizeExceeded if too large."""
"""Wraps a file-like object, raising MaxSizeExceeded if too large.
:param rfile: ``file`` of a limited size
:param int maxlen: maximum length of the file being read
"""
def __init__(self, rfile, maxlen):
"""Initialize SizeCheckWrapper instance.
Args:
rfile (file): file of a limited size
maxlen (int): maximum length of the file being read
"""
"""Initialize SizeCheckWrapper instance."""
self.rfile = rfile
self.maxlen = maxlen
self.bytes_read = 0
@ -266,14 +271,12 @@ class SizeCheckWrapper:
raise errors.MaxSizeExceeded()
def read(self, size=None):
"""Read a chunk from rfile buffer and return it.
"""Read a chunk from ``rfile`` buffer and return it.
Args:
size (int): amount of data to read
Returns:
bytes: Chunk from rfile, limited by size if specified.
:param int size: amount of data to read
:returns: chunk from ``rfile``, limited by size if specified
:rtype: bytes
"""
data = self.rfile.read(size)
self.bytes_read += len(data)
@ -281,14 +284,12 @@ class SizeCheckWrapper:
return data
def readline(self, size=None):
"""Read a single line from rfile buffer and return it.
"""Read a single line from ``rfile`` buffer and return it.
Args:
size (int): minimum amount of data to read
Returns:
bytes: One line from rfile.
:param int size: minimum amount of data to read
:returns: one line from ``rfile``
:rtype: bytes
"""
if size is not None:
data = self.rfile.readline(size)
@ -309,14 +310,12 @@ class SizeCheckWrapper:
return EMPTY.join(res)
def readlines(self, sizehint=0):
"""Read all lines from rfile buffer and return them.
"""Read all lines from ``rfile`` buffer and return them.
Args:
sizehint (int): hint of minimum amount of data to read
Returns:
list[bytes]: Lines of bytes read from rfile.
:param int sizehint: hint of minimum amount of data to read
:returns: lines of bytes read from ``rfile``
:rtype: list[bytes]
"""
# Shamelessly stolen from StringIO
total = 0
@ -331,7 +330,7 @@ class SizeCheckWrapper:
return lines
def close(self):
"""Release resources allocated for rfile."""
"""Release resources allocated for ``rfile``."""
self.rfile.close()
def __iter__(self):
@ -349,28 +348,24 @@ class SizeCheckWrapper:
class KnownLengthRFile:
"""Wraps a file-like object, returning an empty string when exhausted."""
"""Wraps a file-like object, returning an empty string when exhausted.
:param rfile: ``file`` of a known size
:param int content_length: length of the file being read
"""
def __init__(self, rfile, content_length):
"""Initialize KnownLengthRFile instance.
Args:
rfile (file): file of a known size
content_length (int): length of the file being read
"""
"""Initialize KnownLengthRFile instance."""
self.rfile = rfile
self.remaining = content_length
def read(self, size=None):
"""Read a chunk from rfile buffer and return it.
"""Read a chunk from ``rfile`` buffer and return it.
Args:
size (int): amount of data to read
Returns:
bytes: Chunk from rfile, limited by size if specified.
:param int size: amount of data to read
:rtype: bytes
:returns: chunk from ``rfile``, limited by size if specified
"""
if self.remaining == 0:
return b''
@ -384,14 +379,12 @@ class KnownLengthRFile:
return data
def readline(self, size=None):
"""Read a single line from rfile buffer and return it.
"""Read a single line from ``rfile`` buffer and return it.
Args:
size (int): minimum amount of data to read
Returns:
bytes: One line from rfile.
:param int size: minimum amount of data to read
:returns: one line from ``rfile``
:rtype: bytes
"""
if self.remaining == 0:
return b''
@ -405,14 +398,12 @@ class KnownLengthRFile:
return data
def readlines(self, sizehint=0):
"""Read all lines from rfile buffer and return them.
"""Read all lines from ``rfile`` buffer and return them.
Args:
sizehint (int): hint of minimum amount of data to read
Returns:
list[bytes]: Lines of bytes read from rfile.
:param int sizehint: hint of minimum amount of data to read
:returns: lines of bytes read from ``rfile``
:rtype: list[bytes]
"""
# Shamelessly stolen from StringIO
total = 0
@ -427,7 +418,7 @@ class KnownLengthRFile:
return lines
def close(self):
"""Release resources allocated for rfile."""
"""Release resources allocated for ``rfile``."""
self.rfile.close()
def __iter__(self):
@ -449,16 +440,14 @@ class ChunkedRFile:
This class is intended to provide a conforming wsgi.input value for
request entities that have been encoded with the 'chunked' transfer
encoding.
:param rfile: file encoded with the 'chunked' transfer encoding
:param int maxlen: maximum length of the file being read
:param int bufsize: size of the buffer used to read the file
"""
def __init__(self, rfile, maxlen, bufsize=8192):
"""Initialize ChunkedRFile instance.
Args:
rfile (file): file encoded with the 'chunked' transfer encoding
maxlen (int): maximum length of the file being read
bufsize (int): size of the buffer used to read the file
"""
"""Initialize ChunkedRFile instance."""
self.rfile = rfile
self.maxlen = maxlen
self.bytes_read = 0
@ -484,7 +473,10 @@ class ChunkedRFile:
chunk_size = line.pop(0)
chunk_size = int(chunk_size, 16)
except ValueError:
raise ValueError('Bad chunked transfer size: ' + repr(chunk_size))
raise ValueError(
'Bad chunked transfer size: {chunk_size!r}'.
format(chunk_size=chunk_size),
)
if chunk_size <= 0:
self.closed = True
@ -507,14 +499,12 @@ class ChunkedRFile:
)
def read(self, size=None):
"""Read a chunk from rfile buffer and return it.
"""Read a chunk from ``rfile`` buffer and return it.
Args:
size (int): amount of data to read
Returns:
bytes: Chunk from rfile, limited by size if specified.
:param int size: amount of data to read
:returns: chunk from ``rfile``, limited by size if specified
:rtype: bytes
"""
data = EMPTY
@ -540,14 +530,12 @@ class ChunkedRFile:
self.buffer = EMPTY
def readline(self, size=None):
"""Read a single line from rfile buffer and return it.
"""Read a single line from ``rfile`` buffer and return it.
Args:
size (int): minimum amount of data to read
Returns:
bytes: One line from rfile.
:param int size: minimum amount of data to read
:returns: one line from ``rfile``
:rtype: bytes
"""
data = EMPTY
@ -583,14 +571,12 @@ class ChunkedRFile:
self.buffer = self.buffer[newline_pos:]
def readlines(self, sizehint=0):
"""Read all lines from rfile buffer and return them.
"""Read all lines from ``rfile`` buffer and return them.
Args:
sizehint (int): hint of minimum amount of data to read
Returns:
list[bytes]: Lines of bytes read from rfile.
:param int sizehint: hint of minimum amount of data to read
:returns: lines of bytes read from ``rfile``
:rtype: list[bytes]
"""
# Shamelessly stolen from StringIO
total = 0
@ -635,7 +621,7 @@ class ChunkedRFile:
yield line
def close(self):
"""Release resources allocated for rfile."""
"""Release resources allocated for ``rfile``."""
self.rfile.close()
@ -744,7 +730,7 @@ class HTTPRequest:
self.ready = True
def read_request_line(self):
def read_request_line(self): # noqa: C901 # FIXME
"""Read and parse first line of the HTTP request.
Returns:
@ -845,7 +831,7 @@ class HTTPRequest:
# `urlsplit()` above parses "example.com:3128" as path part of URI.
# this is a workaround, which makes it detect netloc correctly
uri_split = urllib.parse.urlsplit(b'//' + uri)
uri_split = urllib.parse.urlsplit(b''.join((b'//', uri)))
_scheme, _authority, _path, _qs, _fragment = uri_split
_port = EMPTY
try:
@ -975,8 +961,14 @@ class HTTPRequest:
return True
def read_request_headers(self):
"""Read self.rfile into self.inheaders. Return success."""
def read_request_headers(self): # noqa: C901 # FIXME
"""Read ``self.rfile`` into ``self.inheaders``.
Ref: :py:attr:`self.inheaders <HTTPRequest.outheaders>`.
:returns: success status
:rtype: bool
"""
# then all the http headers
try:
self.header_reader(self.rfile, self.inheaders)
@ -1054,8 +1046,10 @@ class HTTPRequest:
# Don't use simple_response here, because it emits headers
# we don't want. See
# https://github.com/cherrypy/cherrypy/issues/951
msg = self.server.protocol.encode('ascii')
msg += b' 100 Continue\r\n\r\n'
msg = b''.join((
self.server.protocol.encode('ascii'), SPACE, b'100 Continue',
CRLF, CRLF,
))
try:
self.conn.wfile.write(msg)
except socket.error as ex:
@ -1138,10 +1132,11 @@ class HTTPRequest:
else:
self.conn.wfile.write(chunk)
def send_headers(self):
def send_headers(self): # noqa: C901 # FIXME
"""Assert, process, and send the HTTP response message-headers.
You must set self.status, and self.outheaders before calling this.
You must set ``self.status``, and :py:attr:`self.outheaders
<HTTPRequest.outheaders>` before calling this.
"""
hkeys = [key.lower() for key, value in self.outheaders]
status = int(self.status[:3])
@ -1168,6 +1163,12 @@ class HTTPRequest:
# Closing the conn is the only way to determine len.
self.close_connection = True
# Override the decision to not close the connection if the connection
# manager doesn't have space for it.
if not self.close_connection:
can_keep = self.server.can_add_keepalive_connection
self.close_connection = not can_keep
if b'connection' not in hkeys:
if self.response_protocol == 'HTTP/1.1':
# Both server and client are HTTP/1.1 or better
@ -1178,6 +1179,14 @@ class HTTPRequest:
if not self.close_connection:
self.outheaders.append((b'Connection', b'Keep-Alive'))
if (b'Connection', b'Keep-Alive') in self.outheaders:
self.outheaders.append((
b'Keep-Alive',
u'timeout={connection_timeout}'.
format(connection_timeout=self.server.timeout).
encode('ISO-8859-1'),
))
if (not self.close_connection) and (not self.chunked_read):
# Read any remaining request body data on the socket.
# "If an origin server receives a request that does not include an
@ -1228,9 +1237,7 @@ class HTTPConnection:
peercreds_resolve_enabled = False
# Fields set by ConnectionManager.
closeable = False
last_used = None
ready_with_data = False
def __init__(self, server, sock, makefile=MakeFile):
"""Initialize HTTPConnection instance.
@ -1259,7 +1266,7 @@ class HTTPConnection:
lru_cache(maxsize=1)(self.get_peer_creds)
)
def communicate(self):
def communicate(self): # noqa: C901 # FIXME
"""Read each request and respond appropriately.
Returns true if the connection should be kept open.
@ -1351,6 +1358,9 @@ class HTTPConnection:
if not self.linger:
self._close_kernel_socket()
# close the socket file descriptor
# (will be closed in the OS if there is no
# other reference to the underlying socket)
self.socket.close()
else:
# On the other hand, sometimes we want to hang around for a bit
@ -1426,12 +1436,12 @@ class HTTPConnection:
return gid
def resolve_peer_creds(self): # LRU cached on per-instance basis
"""Return the username and group tuple of the peercreds if available.
"""Look up the username and group tuple of the ``PEERCREDS``.
Raises:
NotImplementedError: in case of unsupported OS
RuntimeError: in case of UID/GID lookup unsupported or disabled
:returns: the username and group tuple of the ``PEERCREDS``
:raises NotImplementedError: if the OS is unsupported
:raises RuntimeError: if UID/GID lookup is unsupported or disabled
"""
if not IS_UID_GID_RESOLVABLE:
raise NotImplementedError(
@ -1462,18 +1472,20 @@ class HTTPConnection:
return group
def _close_kernel_socket(self):
"""Close kernel socket in outdated Python versions.
"""Terminate the connection at the transport level."""
# Honor ``sock_shutdown`` for PyOpenSSL connections.
shutdown = getattr(
self.socket, 'sock_shutdown',
self.socket.shutdown,
)
On old Python versions,
Python's socket module does NOT call close on the kernel
socket when you call socket.close(). We do so manually here
because we want this server to send a FIN TCP segment
immediately. Note this must be called *before* calling
socket.close(), because the latter drops its reference to
the kernel socket.
"""
if six.PY2 and hasattr(self.socket, '_sock'):
self.socket._sock.close()
try:
shutdown(socket.SHUT_RDWR) # actually send a TCP FIN
except errors.acceptable_sock_shutdown_exceptions:
pass
except socket.error as e:
if e.errno not in errors.acceptable_sock_shutdown_error_codes:
raise
class HTTPServer:
@ -1515,7 +1527,12 @@ class HTTPServer:
timeout = 10
"""The timeout in seconds for accepted connections (default 10)."""
version = 'Cheroot/' + __version__
expiration_interval = 0.5
"""The interval, in seconds, at which the server checks for
expired connections (default 0.5).
"""
version = 'Cheroot/{version!s}'.format(version=__version__)
"""A version string for the HTTPServer."""
software = None
@ -1540,16 +1557,23 @@ class HTTPServer:
"""The class to use for handling HTTP connections."""
ssl_adapter = None
"""An instance of ssl.Adapter (or a subclass).
"""An instance of ``ssl.Adapter`` (or a subclass).
You must have the corresponding SSL driver library installed.
Ref: :py:class:`ssl.Adapter <cheroot.ssl.Adapter>`.
You must have the corresponding TLS driver library installed.
"""
peercreds_enabled = False
"""If True, peer cred lookup can be performed via UNIX domain socket."""
"""
If :py:data:`True`, peer creds will be looked up via UNIX domain socket.
"""
peercreds_resolve_enabled = False
"""If True, username/group will be looked up in the OS from peercreds."""
"""
If :py:data:`True`, username/group will be looked up in the OS from
``PEERCREDS``-provided IDs.
"""
keep_alive_conn_limit = 10
"""The maximum number of waiting keep-alive connections that will be kept open.
@ -1577,7 +1601,6 @@ class HTTPServer:
self.requests = threadpool.ThreadPool(
self, min=minthreads or 1, max=maxthreads,
)
self.connections = connections.ConnectionManager(self)
if not server_name:
server_name = self.version
@ -1603,25 +1626,29 @@ class HTTPServer:
'Threads Idle': lambda s: getattr(self.requests, 'idle', None),
'Socket Errors': 0,
'Requests': lambda s: (not s['Enabled']) and -1 or sum(
[w['Requests'](w) for w in s['Worker Threads'].values()], 0,
(w['Requests'](w) for w in s['Worker Threads'].values()), 0,
),
'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum(
[w['Bytes Read'](w) for w in s['Worker Threads'].values()], 0,
(w['Bytes Read'](w) for w in s['Worker Threads'].values()), 0,
),
'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum(
[w['Bytes Written'](w) for w in s['Worker Threads'].values()],
(w['Bytes Written'](w) for w in s['Worker Threads'].values()),
0,
),
'Work Time': lambda s: (not s['Enabled']) and -1 or sum(
[w['Work Time'](w) for w in s['Worker Threads'].values()], 0,
(w['Work Time'](w) for w in s['Worker Threads'].values()), 0,
),
'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum(
[w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6)
for w in s['Worker Threads'].values()], 0,
(
w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6)
for w in s['Worker Threads'].values()
), 0,
),
'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum(
[w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6)
for w in s['Worker Threads'].values()], 0,
(
w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6)
for w in s['Worker Threads'].values()
), 0,
),
'Worker Threads': {},
}
@ -1645,17 +1672,27 @@ class HTTPServer:
def bind_addr(self):
"""Return the interface on which to listen for connections.
For TCP sockets, a (host, port) tuple. Host values may be any IPv4
or IPv6 address, or any valid hostname. The string 'localhost' is a
synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
The string '0.0.0.0' is a special IPv4 entry meaning "any active
interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
IPv6. The empty string or None are not allowed.
For TCP sockets, a (host, port) tuple. Host values may be any
:term:`IPv4` or :term:`IPv6` address, or any valid hostname.
The string 'localhost' is a synonym for '127.0.0.1' (or '::1',
if your hosts file prefers :term:`IPv6`).
The string '0.0.0.0' is a special :term:`IPv4` entry meaning
"any active interface" (INADDR_ANY), and '::' is the similar
IN6ADDR_ANY for :term:`IPv6`.
The empty string or :py:data:`None` are not allowed.
For UNIX sockets, supply the filename as a string.
For UNIX sockets, supply the file name as a string.
Systemd socket activation is automatic and doesn't require tempering
with this variable.
.. glossary::
:abbr:`IPv4 (Internet Protocol version 4)`
Internet Protocol version 4
:abbr:`IPv6 (Internet Protocol version 6)`
Internet Protocol version 6
"""
return self._bind_addr
@ -1695,7 +1732,7 @@ class HTTPServer:
self.stop()
raise
def prepare(self):
def prepare(self): # noqa: C901 # FIXME
"""Prepare server to serving requests.
It binds a socket's port, setups the socket to ``listen()`` and does
@ -1757,6 +1794,9 @@ class HTTPServer:
self.socket.settimeout(1)
self.socket.listen(self.request_queue_size)
# must not be accessed once stop() has been called
self._connections = connections.ConnectionManager(self)
# Create worker threads
self.requests.start()
@ -1765,23 +1805,24 @@ class HTTPServer:
def serve(self):
"""Serve requests, after invoking :func:`prepare()`."""
while self.ready:
while self.ready and not self.interrupt:
try:
self.tick()
self._connections.run(self.expiration_interval)
except (KeyboardInterrupt, SystemExit):
raise
except Exception:
self.error_log(
'Error in HTTPServer.tick', level=logging.ERROR,
'Error in HTTPServer.serve', level=logging.ERROR,
traceback=True,
)
# raise exceptions reported by any worker threads,
# such that the exception is raised from the serve() thread.
if self.interrupt:
while self._stopping_for_interrupt:
time.sleep(0.1)
if self.interrupt:
while self.interrupt is True:
# Wait for self.stop() to complete. See _set_interrupt.
time.sleep(0.1)
if self.interrupt:
raise self.interrupt
raise self.interrupt
def start(self):
"""Run the server forever.
@ -1795,6 +1836,31 @@ class HTTPServer:
self.prepare()
self.serve()
@contextlib.contextmanager
def _run_in_thread(self):
"""Context manager for running this server in a thread."""
self.prepare()
thread = threading.Thread(target=self.serve)
thread.setDaemon(True)
thread.start()
try:
yield thread
finally:
self.stop()
@property
def can_add_keepalive_connection(self):
"""Flag whether it is allowed to add a new keep-alive connection."""
return self.ready and self._connections.can_add_keepalive_connection
def put_conn(self, conn):
"""Put an idle connection back into the ConnectionManager."""
if self.ready:
self._connections.put(conn)
else:
# server is shutting down, just close it
conn.close()
def error_log(self, msg='', level=20, traceback=False):
"""Write error message to log.
@ -1804,7 +1870,7 @@ class HTTPServer:
traceback (bool): add traceback to output or not
"""
# Override this in subclasses as desired
sys.stderr.write(msg + '\n')
sys.stderr.write('{msg!s}\n'.format(msg=msg))
sys.stderr.flush()
if traceback:
tblines = traceback_.format_exc()
@ -1822,7 +1888,7 @@ class HTTPServer:
self.bind_addr = self.resolve_real_bind_addr(sock)
return sock
def bind_unix_socket(self, bind_addr):
def bind_unix_socket(self, bind_addr): # noqa: C901 # FIXME
"""Create (or recreate) a UNIX socket object."""
if IS_WINDOWS:
"""
@ -1965,7 +2031,7 @@ class HTTPServer:
@staticmethod
def resolve_real_bind_addr(socket_):
"""Retrieve actual bind addr from bound socket."""
"""Retrieve actual bind address from bound socket."""
# FIXME: keep requested bind_addr separate real bound_addr (port
# is different in case of ephemeral port 0)
bind_addr = socket_.getsockname()
@ -1985,40 +2051,49 @@ class HTTPServer:
return bind_addr
def tick(self):
"""Accept a new connection and put it on the Queue."""
if not self.ready:
return
conn = self.connections.get_conn(self.socket)
if conn:
try:
self.requests.put(conn)
except queue.Full:
# Just drop the conn. TODO: write 503 back?
conn.close()
self.connections.expire()
def process_conn(self, conn):
"""Process an incoming HTTPConnection."""
try:
self.requests.put(conn)
except queue.Full:
# Just drop the conn. TODO: write 503 back?
conn.close()
@property
def interrupt(self):
"""Flag interrupt of the server."""
return self._interrupt
@property
def _stopping_for_interrupt(self):
"""Return whether the server is responding to an interrupt."""
return self._interrupt is _STOPPING_FOR_INTERRUPT
@interrupt.setter
def interrupt(self, interrupt):
"""Perform the shutdown of this server and save the exception."""
self._interrupt = True
"""Perform the shutdown of this server and save the exception.
Typically invoked by a worker thread in
:py:mod:`~cheroot.workers.threadpool`, the exception is raised
from the thread running :py:meth:`serve` once :py:meth:`stop`
has completed.
"""
self._interrupt = _STOPPING_FOR_INTERRUPT
self.stop()
self._interrupt = interrupt
def stop(self):
def stop(self): # noqa: C901 # FIXME
"""Gracefully shutdown a server that is serving forever."""
if not self.ready:
return # already stopped
self.ready = False
if self._start_time is not None:
self._run_time += (time.time() - self._start_time)
self._start_time = None
self._connections.stop()
sock = getattr(self, 'socket', None)
if sock:
if not isinstance(
@ -2060,7 +2135,7 @@ class HTTPServer:
sock.close()
self.socket = None
self.connections.close()
self._connections.close()
self.requests.stop(self.shutdown_timeout)
@ -2108,7 +2183,9 @@ def get_ssl_adapter_class(name='builtin'):
try:
adapter = getattr(mod, attr_name)
except AttributeError:
raise AttributeError("'%s' object has no attribute '%s'"
% (mod_path, attr_name))
raise AttributeError(
"'%s' object has no attribute '%s'"
% (mod_path, attr_name),
)
return adapter