From 3d378eb583cac54ba09d0c770034aa9855b58ee1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Dec 2022 15:58:54 -0800 Subject: [PATCH] Bump cheroot from 8.6.0 to 9.0.0 (#1903) * Bump cheroot from 8.6.0 to 9.0.0 Bumps [cheroot](https://github.com/cherrypy/cheroot) from 8.6.0 to 9.0.0. - [Release notes](https://github.com/cherrypy/cheroot/releases) - [Changelog](https://github.com/cherrypy/cheroot/blob/main/CHANGES.rst) - [Commits](https://github.com/cherrypy/cheroot/compare/v8.6.0...v9.0.0) --- updated-dependencies: - dependency-name: cheroot dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Update cheroot==9.0.0 Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> [skip ci] --- lib/cheroot/__init__.py | 9 +- lib/cheroot/_compat.py | 93 +----- lib/cheroot/_compat.pyi | 21 ++ lib/cheroot/cli.py | 12 +- lib/cheroot/connections.py | 17 +- lib/cheroot/errors.py | 7 +- lib/cheroot/errors.pyi | 4 +- lib/cheroot/makefile.py | 439 +++-------------------------- lib/cheroot/makefile.pyi | 13 - lib/cheroot/server.py | 52 +--- lib/cheroot/ssl/__init__.py | 8 +- lib/cheroot/ssl/builtin.py | 13 +- lib/cheroot/ssl/builtin.pyi | 3 +- lib/cheroot/ssl/pyopenssl.py | 8 +- lib/cheroot/ssl/pyopenssl.pyi | 11 +- lib/cheroot/test/_pytest_plugin.py | 18 +- lib/cheroot/test/conftest.py | 18 +- lib/cheroot/test/helper.py | 22 +- lib/cheroot/test/test__compat.py | 9 +- lib/cheroot/test/test_cli.py | 9 - lib/cheroot/test/test_conn.py | 48 ++-- lib/cheroot/test/test_core.py | 25 +- lib/cheroot/test/test_dispatch.py | 4 - lib/cheroot/test/test_makefile.py | 3 - lib/cheroot/test/test_server.py | 40 ++- lib/cheroot/test/test_ssl.py | 101 ++----- lib/cheroot/test/test_wsgi.py | 2 + lib/cheroot/test/webtest.py | 52 +--- lib/cheroot/testing.py | 16 +- lib/cheroot/workers/threadpool.py | 11 +- lib/cheroot/wsgi.py | 41 +-- lib/cheroot/wsgi.pyi | 7 + requirements.txt | 2 +- 33 files changed, 287 insertions(+), 851 deletions(-) create mode 100644 lib/cheroot/_compat.pyi diff --git a/lib/cheroot/__init__.py b/lib/cheroot/__init__.py index 30d38cab..aac9cd98 100644 --- a/lib/cheroot/__init__.py +++ b/lib/cheroot/__init__.py @@ -1,15 +1,12 @@ """High-performance, pure-Python HTTP server used by CherryPy.""" -from __future__ import absolute_import, division, print_function -__metaclass__ = type - try: - import pkg_resources + from importlib import metadata except ImportError: - pass + import importlib_metadata as metadata # noqa: WPS440 try: - __version__ = pkg_resources.get_distribution('cheroot').version + __version__ = metadata.version('cheroot') except Exception: __version__ = 'unknown' diff --git a/lib/cheroot/_compat.py b/lib/cheroot/_compat.py index 10dcdefa..20c993de 100644 --- a/lib/cheroot/_compat.py +++ b/lib/cheroot/_compat.py @@ -1,19 +1,9 @@ # pylint: disable=unused-import """Compatibility code for using Cheroot with various versions of Python.""" -from __future__ import absolute_import, division, print_function -__metaclass__ = type - import os import platform -import re -import six - -try: - import selectors # lgtm [py/unused-import] -except ImportError: - import selectors2 as selectors # noqa: F401 # lgtm [py/unused-import] try: import ssl @@ -22,20 +12,6 @@ try: except ImportError: IS_ABOVE_OPENSSL10 = None -# contextlib.suppress was added in Python 3.4 -try: - from contextlib import suppress -except ImportError: - from contextlib import contextmanager - - @contextmanager - def suppress(*exceptions): - """Return a context manager that suppresses the `exceptions`.""" - try: - yield - except exceptions: - pass - IS_CI = bool(os.getenv('CI')) IS_GITHUB_ACTIONS_WORKFLOW = bool(os.getenv('GITHUB_WORKFLOW')) @@ -53,53 +29,23 @@ PLATFORM_ARCH = platform.machine() IS_PPC = PLATFORM_ARCH.startswith('ppc') -if not six.PY2: - def ntob(n, encoding='ISO-8859-1'): - """Return the native string as bytes in the given encoding.""" - assert_native(n) - # In Python 3, the native string type is unicode - return n.encode(encoding) +def ntob(n, encoding='ISO-8859-1'): + """Return the native string as bytes in the given encoding.""" + assert_native(n) + # In Python 3, the native string type is unicode + return n.encode(encoding) - def ntou(n, encoding='ISO-8859-1'): - """Return the native string as Unicode with the given encoding.""" - assert_native(n) - # In Python 3, the native string type is unicode - return n - def bton(b, encoding='ISO-8859-1'): - """Return the byte string as native string in the given encoding.""" - return b.decode(encoding) -else: - # Python 2 - def ntob(n, encoding='ISO-8859-1'): - """Return the native string as bytes in the given encoding.""" - assert_native(n) - # In Python 2, the native string type is bytes. Assume it's already - # in the given encoding, which for ISO-8859-1 is almost always what - # was intended. - return n +def ntou(n, encoding='ISO-8859-1'): + """Return the native string as Unicode with the given encoding.""" + assert_native(n) + # In Python 3, the native string type is unicode + return n - def ntou(n, encoding='ISO-8859-1'): - """Return the native string as Unicode with the given encoding.""" - assert_native(n) - # In Python 2, the native string type is bytes. - # First, check for the special encoding 'escape'. The test suite uses - # this to signal that it wants to pass a string with embedded \uXXXX - # escapes, but without having to prefix it with u'' for Python 2, - # but no prefix for Python 3. - if encoding == 'escape': - return re.sub( - r'\\u([0-9a-zA-Z]{4})', - lambda m: six.unichr(int(m.group(1), 16)), - n.decode('ISO-8859-1'), - ) - # Assume it's already in the given encoding, which for ISO-8859-1 - # is almost always what was intended. - return n.decode(encoding) - def bton(b, encoding='ISO-8859-1'): - """Return the byte string as native string in the given encoding.""" - return b +def bton(b, encoding='ISO-8859-1'): + """Return the byte string as native string in the given encoding.""" + return b.decode(encoding) def assert_native(n): @@ -113,17 +59,6 @@ def assert_native(n): raise TypeError('n must be a native str (got %s)' % type(n).__name__) -if not six.PY2: - """Python 3 has :py:class:`memoryview` builtin.""" - # Python 2.7 has it backported, but socket.write() does - # str(memoryview(b'0' * 100)) -> - # instead of accessing it correctly. - memoryview = memoryview -else: - """Link :py:class:`memoryview` to buffer under Python 2.""" - memoryview = buffer # noqa: F821 - - def extract_bytes(mv): r"""Retrieve bytes out of the given input buffer. @@ -138,7 +73,7 @@ def extract_bytes(mv): or :py:class:`bytes` """ if isinstance(mv, memoryview): - return bytes(mv) if six.PY2 else mv.tobytes() + return mv.tobytes() if isinstance(mv, bytes): return mv diff --git a/lib/cheroot/_compat.pyi b/lib/cheroot/_compat.pyi new file mode 100644 index 00000000..023bad8c --- /dev/null +++ b/lib/cheroot/_compat.pyi @@ -0,0 +1,21 @@ +from typing import Any, ContextManager, Optional, Type, Union + +def suppress(*exceptions: Type[BaseException]) -> ContextManager[None]: ... + +IS_ABOVE_OPENSSL10: Optional[bool] +IS_CI: bool +IS_GITHUB_ACTIONS_WORKFLOW: bool +IS_PYPY: bool +SYS_PLATFORM: str +IS_WINDOWS: bool +IS_LINUX: bool +IS_MACOS: bool +PLATFORM_ARCH: str +IS_PPC: bool + +def ntob(n: str, encoding: str = ...) -> bytes: ... +def ntou(n: str, encoding: str = ...) -> str: ... +def bton(b: bytes, encoding: str = ...) -> str: ... +def assert_native(n: str) -> None: ... + +def extract_bytes(mv: Union[memoryview, bytes]) -> bytes: ... diff --git a/lib/cheroot/cli.py b/lib/cheroot/cli.py index 4607e226..cd168e91 100644 --- a/lib/cheroot/cli.py +++ b/lib/cheroot/cli.py @@ -28,18 +28,14 @@ Basic usage: """ import argparse -from importlib import import_module import os import sys - -import six +import urllib.parse # noqa: WPS301 +from importlib import import_module +from contextlib import suppress from . import server from . import wsgi -from ._compat import suppress - - -__metaclass__ = type class BindLocation: @@ -143,7 +139,7 @@ def parse_wsgi_bind_location(bind_addr_string): return AbstractSocket(bind_addr_string[1:]) # try and match for an IP/hostname and port - match = six.moves.urllib.parse.urlparse( + match = urllib.parse.urlparse( '//{addr}'.format(addr=bind_addr_string), ) try: diff --git a/lib/cheroot/connections.py b/lib/cheroot/connections.py index 181e3731..9b6366e5 100644 --- a/lib/cheroot/connections.py +++ b/lib/cheroot/connections.py @@ -1,22 +1,17 @@ """Utilities to manage open connections.""" -from __future__ import absolute_import, division, print_function -__metaclass__ = type - import io import os import socket import threading import time +import selectors +from contextlib import suppress from . import errors -from ._compat import selectors -from ._compat import suppress from ._compat import IS_WINDOWS from .makefile import MakeFile -import six - try: import fcntl except ImportError: @@ -310,8 +305,7 @@ class ConnectionManager: msg, ] - sock_to_make = s if not six.PY2 else s._sock - wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE) + wfile = mf(s, 'wb', io.DEFAULT_BUFFER_SIZE) try: wfile.write(''.join(buf).encode('ISO-8859-1')) except socket.error as ex: @@ -327,10 +321,7 @@ class ConnectionManager: conn = self.server.ConnectionClass(self.server, s, mf) - if not isinstance( - self.server.bind_addr, - (six.text_type, six.binary_type), - ): + if not isinstance(self.server.bind_addr, (str, bytes)): # optional values # Until we do DNS lookups, omit REMOTE_HOST if addr is None: # sometimes this can happen diff --git a/lib/cheroot/errors.py b/lib/cheroot/errors.py index e00629f8..046263ad 100644 --- a/lib/cheroot/errors.py +++ b/lib/cheroot/errors.py @@ -1,17 +1,14 @@ # -*- coding: utf-8 -*- """Collection of exceptions raised and/or processed by Cheroot.""" -from __future__ import absolute_import, division, print_function -__metaclass__ = type - import errno import sys class MaxSizeExceeded(Exception): - """Exception raised when a client sends more data then acceptable within limit. + """Exception raised when a client sends more data then allowed under limit. - Depends on ``request.body.maxbytes`` config option if used within CherryPy + Depends on ``request.body.maxbytes`` config option if used within CherryPy. """ diff --git a/lib/cheroot/errors.pyi b/lib/cheroot/errors.pyi index e78a7585..18669568 100644 --- a/lib/cheroot/errors.pyi +++ b/lib/cheroot/errors.pyi @@ -1,4 +1,4 @@ -from typing import Any, List, Set, Tuple +from typing import List, Set, Tuple, Type class MaxSizeExceeded(Exception): ... class NoSSLError(Exception): ... @@ -10,4 +10,4 @@ socket_error_eintr: List[int] socket_errors_to_ignore: List[int] socket_errors_nonblocking: List[int] acceptable_sock_shutdown_error_codes: Set[int] -acceptable_sock_shutdown_exceptions: Tuple[Exception] +acceptable_sock_shutdown_exceptions: Tuple[Type[Exception], ...] diff --git a/lib/cheroot/makefile.py b/lib/cheroot/makefile.py index 1383c658..77878c13 100644 --- a/lib/cheroot/makefile.py +++ b/lib/cheroot/makefile.py @@ -1,21 +1,9 @@ """Socket file object.""" -from __future__ import absolute_import, division, print_function -__metaclass__ = type - import socket -try: - # prefer slower Python-based io module - import _pyio as io -except ImportError: - # Python 2.6 - import io - -import six - -from . import errors -from ._compat import extract_bytes, memoryview +# prefer slower Python-based io module +import _pyio as io # Write only 16K at a time to sockets @@ -48,400 +36,41 @@ class BufferedWriter(io.BufferedWriter): del self._write_buf[:n] -class MakeFile_PY2(getattr(socket, '_fileobject', object)): - """Faux file object attached to a socket object.""" +class StreamReader(io.BufferedReader): + """Socket stream reader.""" - def __init__(self, *args, **kwargs): - """Initialize faux file object.""" + def __init__(self, sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): + """Initialize socket stream reader.""" + super().__init__(socket.SocketIO(sock, mode), bufsize) self.bytes_read = 0 + + def read(self, *args, **kwargs): + """Capture bytes read.""" + val = super().read(*args, **kwargs) + self.bytes_read += len(val) + return val + + def has_data(self): + """Return true if there is buffered data to read.""" + return len(self._read_buf) > self._read_pos + + +class StreamWriter(BufferedWriter): + """Socket stream writer.""" + + def __init__(self, sock, mode='w', bufsize=io.DEFAULT_BUFFER_SIZE): + """Initialize socket stream writer.""" + super().__init__(socket.SocketIO(sock, mode), bufsize) self.bytes_written = 0 - socket._fileobject.__init__(self, *args, **kwargs) - self._refcount = 0 - def _reuse(self): - self._refcount += 1 - - def _drop(self): - if self._refcount < 0: - self.close() - else: - self._refcount -= 1 - - def write(self, data): - """Send entire data contents for non-blocking sockets.""" - bytes_sent = 0 - data_mv = memoryview(data) - payload_size = len(data_mv) - while bytes_sent < payload_size: - try: - bytes_sent += self.send( - data_mv[bytes_sent:bytes_sent + SOCK_WRITE_BLOCKSIZE], - ) - except socket.error as e: - if e.args[0] not in errors.socket_errors_nonblocking: - raise - - def send(self, data): - """Send some part of message to the socket.""" - bytes_sent = self._sock.send(extract_bytes(data)) - self.bytes_written += bytes_sent - return bytes_sent - - def flush(self): - """Write all data from buffer to socket and reset write buffer.""" - if self._wbuf: - buffer = ''.join(self._wbuf) - self._wbuf = [] - self.write(buffer) - - def recv(self, size): - """Receive message of a size from the socket.""" - while True: - try: - data = self._sock.recv(size) - self.bytes_read += len(data) - return data - except socket.error as e: - what = ( - e.args[0] not in errors.socket_errors_nonblocking - and e.args[0] not in errors.socket_error_eintr - ) - if what: - raise - - class FauxSocket: - """Faux socket with the minimal interface required by pypy.""" - - def _reuse(self): - pass - - _fileobject_uses_str_type = six.PY2 and isinstance( - socket._fileobject(FauxSocket())._rbuf, six.string_types, - ) - - # FauxSocket is no longer needed - del FauxSocket - - if not _fileobject_uses_str_type: # noqa: C901 # FIXME - def read(self, size=-1): - """Read data from the socket to buffer.""" - # Use max, disallow tiny reads in a loop as they are very - # inefficient. - # We never leave read() with any leftover data from a new recv() - # call in our internal buffer. - rbufsize = max(self._rbufsize, self.default_bufsize) - # Our use of StringIO rather than lists of string objects returned - # by recv() minimizes memory usage and fragmentation that occurs - # when rbufsize is large compared to the typical return value of - # recv(). - buf = self._rbuf - buf.seek(0, 2) # seek end - if size < 0: - # Read until EOF - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - while True: - data = self.recv(rbufsize) - if not data: - break - buf.write(data) - return buf.getvalue() - else: - # Read until size bytes or EOF seen, whichever comes first - buf_len = buf.tell() - if buf_len >= size: - # Already have size bytes in our buffer? Extract and - # return. - buf.seek(0) - rv = buf.read(size) - self._rbuf = io.BytesIO() - self._rbuf.write(buf.read()) - return rv - - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - while True: - left = size - buf_len - # recv() will malloc the amount of memory given as its - # parameter even though it often returns much less data - # than that. The returned data string is short lived - # as we copy it into a StringIO and free it. This avoids - # fragmentation issues on many platforms. - data = self.recv(left) - if not data: - break - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid buffer data copies when: - # - We have no data in our buffer. - # AND - # - Our call to recv returned exactly the - # number of bytes we were asked to read. - return data - if n == left: - buf.write(data) - del data # explicit free - break - assert n <= left, 'recv(%d) returned %d bytes' % (left, n) - buf.write(data) - buf_len += n - del data # explicit free - # assert buf_len == buf.tell() - return buf.getvalue() - - def readline(self, size=-1): - """Read line from the socket to buffer.""" - buf = self._rbuf - buf.seek(0, 2) # seek end - if buf.tell() > 0: - # check if we already have it in our buffer - buf.seek(0) - bline = buf.readline(size) - if bline.endswith('\n') or len(bline) == size: - self._rbuf = io.BytesIO() - self._rbuf.write(buf.read()) - return bline - del bline - if size < 0: - # Read until \n or EOF, whichever comes first - if self._rbufsize <= 1: - # Speed up unbuffered case - buf.seek(0) - buffers = [buf.read()] - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - data = None - recv = self.recv - while data != '\n': - data = recv(1) - if not data: - break - buffers.append(data) - return ''.join(buffers) - - buf.seek(0, 2) # seek end - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - while True: - data = self.recv(self._rbufsize) - if not data: - break - nl = data.find('\n') - if nl >= 0: - nl += 1 - buf.write(data[:nl]) - self._rbuf.write(data[nl:]) - del data - break - buf.write(data) - return buf.getvalue() - - else: - # Read until size bytes or \n or EOF seen, whichever comes - # first - buf.seek(0, 2) # seek end - buf_len = buf.tell() - if buf_len >= size: - buf.seek(0) - rv = buf.read(size) - self._rbuf = io.BytesIO() - self._rbuf.write(buf.read()) - return rv - # reset _rbuf. we consume it via buf. - self._rbuf = io.BytesIO() - while True: - data = self.recv(self._rbufsize) - if not data: - break - left = size - buf_len - # did we just receive a newline? - nl = data.find('\n', 0, left) - if nl >= 0: - nl += 1 - # save the excess data to _rbuf - self._rbuf.write(data[nl:]) - if buf_len: - buf.write(data[:nl]) - break - else: - # Shortcut. Avoid data copy through buf when - # returning a substring of our first recv(). - return data[:nl] - n = len(data) - if n == size and not buf_len: - # Shortcut. Avoid data copy through buf when - # returning exactly all of our first recv(). - return data - if n >= left: - buf.write(data[:left]) - self._rbuf.write(data[left:]) - break - buf.write(data) - buf_len += n - # assert buf_len == buf.tell() - return buf.getvalue() - - def has_data(self): - """Return true if there is buffered data to read.""" - return bool(self._rbuf.getvalue()) - - else: - def read(self, size=-1): - """Read data from the socket to buffer.""" - if size < 0: - # Read until EOF - buffers = [self._rbuf] - self._rbuf = '' - if self._rbufsize <= 1: - recv_size = self.default_bufsize - else: - recv_size = self._rbufsize - - while True: - data = self.recv(recv_size) - if not data: - break - buffers.append(data) - return ''.join(buffers) - else: - # Read until size bytes or EOF seen, whichever comes first - data = self._rbuf - buf_len = len(data) - if buf_len >= size: - self._rbuf = data[size:] - return data[:size] - buffers = [] - if data: - buffers.append(data) - self._rbuf = '' - while True: - left = size - buf_len - recv_size = max(self._rbufsize, left) - data = self.recv(recv_size) - if not data: - break - buffers.append(data) - n = len(data) - if n >= left: - self._rbuf = data[left:] - buffers[-1] = data[:left] - break - buf_len += n - return ''.join(buffers) - - def readline(self, size=-1): - """Read line from the socket to buffer.""" - data = self._rbuf - if size < 0: - # Read until \n or EOF, whichever comes first - if self._rbufsize <= 1: - # Speed up unbuffered case - assert data == '' - buffers = [] - while data != '\n': - data = self.recv(1) - if not data: - break - buffers.append(data) - return ''.join(buffers) - nl = data.find('\n') - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - return data[:nl] - buffers = [] - if data: - buffers.append(data) - self._rbuf = '' - while True: - data = self.recv(self._rbufsize) - if not data: - break - buffers.append(data) - nl = data.find('\n') - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - buffers[-1] = data[:nl] - break - return ''.join(buffers) - else: - # Read until size bytes or \n or EOF seen, whichever comes - # first - nl = data.find('\n', 0, size) - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - return data[:nl] - buf_len = len(data) - if buf_len >= size: - self._rbuf = data[size:] - return data[:size] - buffers = [] - if data: - buffers.append(data) - self._rbuf = '' - while True: - data = self.recv(self._rbufsize) - if not data: - break - buffers.append(data) - left = size - buf_len - nl = data.find('\n', 0, left) - if nl >= 0: - nl += 1 - self._rbuf = data[nl:] - buffers[-1] = data[:nl] - break - n = len(data) - if n >= left: - self._rbuf = data[left:] - buffers[-1] = data[:left] - break - buf_len += n - return ''.join(buffers) - - def has_data(self): - """Return true if there is buffered data to read.""" - return bool(self._rbuf) + def write(self, val, *args, **kwargs): + """Capture bytes written.""" + res = super().write(val, *args, **kwargs) + self.bytes_written += len(val) + return res -if not six.PY2: - class StreamReader(io.BufferedReader): - """Socket stream reader.""" - - def __init__(self, sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): - """Initialize socket stream reader.""" - super().__init__(socket.SocketIO(sock, mode), bufsize) - self.bytes_read = 0 - - def read(self, *args, **kwargs): - """Capture bytes read.""" - val = super().read(*args, **kwargs) - self.bytes_read += len(val) - return val - - def has_data(self): - """Return true if there is buffered data to read.""" - return len(self._read_buf) > self._read_pos - - class StreamWriter(BufferedWriter): - """Socket stream writer.""" - - def __init__(self, sock, mode='w', bufsize=io.DEFAULT_BUFFER_SIZE): - """Initialize socket stream writer.""" - super().__init__(socket.SocketIO(sock, mode), bufsize) - self.bytes_written = 0 - - def write(self, val, *args, **kwargs): - """Capture bytes written.""" - res = super().write(val, *args, **kwargs) - self.bytes_written += len(val) - return res - - def MakeFile(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): - """File object attached to a socket object.""" - cls = StreamReader if 'r' in mode else StreamWriter - return cls(sock, mode, bufsize) -else: - StreamReader = StreamWriter = MakeFile = MakeFile_PY2 +def MakeFile(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): + """File object attached to a socket object.""" + cls = StreamReader if 'r' in mode else StreamWriter + return cls(sock, mode, bufsize) diff --git a/lib/cheroot/makefile.pyi b/lib/cheroot/makefile.pyi index 11748505..3f5ea275 100644 --- a/lib/cheroot/makefile.pyi +++ b/lib/cheroot/makefile.pyi @@ -5,19 +5,6 @@ SOCK_WRITE_BLOCKSIZE: int class BufferedWriter(io.BufferedWriter): def write(self, b): ... -class MakeFile_PY2: - bytes_read: int - bytes_written: int - def __init__(self, *args, **kwargs) -> None: ... - def write(self, data) -> None: ... - def send(self, data): ... - def flush(self) -> None: ... - def recv(self, size): ... - class FauxSocket: ... - def read(self, size: int = ...): ... - def readline(self, size: int = ...): ... - def has_data(self): ... - class StreamReader(io.BufferedReader): bytes_read: int def __init__(self, sock, mode: str = ..., bufsize=...) -> None: ... diff --git a/lib/cheroot/server.py b/lib/cheroot/server.py index d92988ab..6b8e37a9 100644 --- a/lib/cheroot/server.py +++ b/lib/cheroot/server.py @@ -65,9 +65,6 @@ And now for a trivial doctest to exercise the test suite True """ -from __future__ import absolute_import, division, print_function -__metaclass__ = type - import os import io import re @@ -78,20 +75,14 @@ import time import traceback as traceback_ import logging import platform +import queue import contextlib import threading - -try: - from functools import lru_cache -except ImportError: - from backports.functools_lru_cache import lru_cache - -import six -from six.moves import queue -from six.moves import urllib +import urllib.parse +from functools import lru_cache from . import connections, errors, __version__ -from ._compat import bton, ntou +from ._compat import bton from ._compat import IS_PPC from .workers import threadpool from .makefile import MakeFile, StreamWriter @@ -606,8 +597,8 @@ class ChunkedRFile: def read_trailer_lines(self): """Read HTTP headers and yield them. - Returns: - Generator: yields CRLF separated lines. + :yields: CRLF separated lines + :ytype: bytes """ if not self.closed: @@ -817,10 +808,6 @@ class HTTPRequest: return False try: - if six.PY2: # FIXME: Figure out better way to do this - # Ref: https://stackoverflow.com/a/196392/595220 (like this?) - """This is a dummy check for unicode in URI.""" - ntou(bton(uri, 'ascii'), 'ascii') scheme, authority, path, qs, fragment = urllib.parse.urlsplit(uri) except UnicodeError: self.simple_response('400 Bad Request', 'Malformed Request-URI') @@ -1120,7 +1107,7 @@ class HTTPRequest: buf.append(CRLF) if msg: - if isinstance(msg, six.text_type): + if isinstance(msg, str): msg = msg.encode('ISO-8859-1') buf.append(msg) @@ -1422,10 +1409,7 @@ class HTTPConnection: https://github.com/daveti/tcpSockHack msdn.microsoft.com/en-us/commandline/wsl/release_notes#build-15025 """ - six.raise_from( # 3.6+: raise RuntimeError from socket_err - RuntimeError, - socket_err, - ) + raise RuntimeError from socket_err else: pid, uid, gid = struct.unpack(PEERCRED_STRUCT_DEF, peer_creds) return pid, uid, gid @@ -1589,7 +1573,7 @@ class HTTPServer: """ keep_alive_conn_limit = 10 - """The maximum number of waiting keep-alive connections that will be kept open. + """Maximum number of waiting keep-alive connections that will be kept open. Default is 10. Set to None to have unlimited connections.""" @@ -1762,13 +1746,13 @@ class HTTPServer: if os.getenv('LISTEN_PID', None): # systemd socket activation self.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) - elif isinstance(self.bind_addr, (six.text_type, six.binary_type)): + elif isinstance(self.bind_addr, (str, bytes)): # AF_UNIX socket try: self.bind_unix_socket(self.bind_addr) except socket.error as serr: msg = '%s -- (%s: %s)' % (msg, self.bind_addr, serr) - six.raise_from(socket.error(msg), serr) + raise socket.error(msg) from serr else: # AF_INET or AF_INET6 socket # Get the correct address family for our host (allows IPv6 @@ -2007,10 +1991,7 @@ class HTTPServer: * https://gavv.github.io/blog/ephemeral-port-reuse/ """ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - if nodelay and not isinstance( - bind_addr, - (six.text_type, six.binary_type), - ): + if nodelay and not isinstance(bind_addr, (str, bytes)): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) if ssl_adapter is not None: @@ -2059,7 +2040,7 @@ class HTTPServer: """ return bind_addr[:2] - if isinstance(bind_addr, six.binary_type): + if isinstance(bind_addr, bytes): bind_addr = bton(bind_addr) return bind_addr @@ -2109,10 +2090,7 @@ class HTTPServer: sock = getattr(self, 'socket', None) if sock: - if not isinstance( - self.bind_addr, - (six.text_type, six.binary_type), - ): + if not isinstance(self.bind_addr, (str, bytes)): # Touch our own socket to make accept() return immediately. try: host, port = sock.getsockname()[:2] @@ -2179,7 +2157,7 @@ ssl_adapters = { def get_ssl_adapter_class(name='builtin'): """Return an SSL adapter class for the given name.""" adapter = ssl_adapters[name.lower()] - if isinstance(adapter, six.string_types): + if isinstance(adapter, str): last_dot = adapter.rfind('.') attr_name = adapter[last_dot + 1:] mod_path = adapter[:last_dot] diff --git a/lib/cheroot/ssl/__init__.py b/lib/cheroot/ssl/__init__.py index d45fd7f1..19b587d0 100644 --- a/lib/cheroot/ssl/__init__.py +++ b/lib/cheroot/ssl/__init__.py @@ -1,15 +1,9 @@ """Implementation of the SSL adapter base interface.""" -from __future__ import absolute_import, division, print_function -__metaclass__ = type - from abc import ABCMeta, abstractmethod -from six import add_metaclass - -@add_metaclass(ABCMeta) -class Adapter: +class Adapter(metaclass=ABCMeta): """Base class for SSL driver library adapters. Required methods: diff --git a/lib/cheroot/ssl/builtin.py b/lib/cheroot/ssl/builtin.py index ff987a71..b22d4ae6 100644 --- a/lib/cheroot/ssl/builtin.py +++ b/lib/cheroot/ssl/builtin.py @@ -7,12 +7,10 @@ To use this module, set ``HTTPServer.ssl_adapter`` to an instance of ``BuiltinSSLAdapter``. """ -from __future__ import absolute_import, division, print_function -__metaclass__ = type - import socket import sys import threading +from contextlib import suppress try: import ssl @@ -27,18 +25,13 @@ except ImportError: except ImportError: DEFAULT_BUFFER_SIZE = -1 -import six - from . import Adapter from .. import errors -from .._compat import IS_ABOVE_OPENSSL10, suppress +from .._compat import IS_ABOVE_OPENSSL10 from ..makefile import StreamReader, StreamWriter from ..server import HTTPServer -if six.PY2: - generic_socket_error = socket.error -else: - generic_socket_error = OSError +generic_socket_error = OSError def _assert_ssl_exc_contains(exc, *msgs): diff --git a/lib/cheroot/ssl/builtin.pyi b/lib/cheroot/ssl/builtin.pyi index fdc656e0..72e45001 100644 --- a/lib/cheroot/ssl/builtin.pyi +++ b/lib/cheroot/ssl/builtin.pyi @@ -1,7 +1,6 @@ from typing import Any from . import Adapter -generic_socket_error: OSError DEFAULT_BUFFER_SIZE: int class BuiltinSSLAdapter(Adapter): @@ -14,5 +13,5 @@ class BuiltinSSLAdapter(Adapter): def context(self, context) -> None: ... def bind(self, sock): ... def wrap(self, sock): ... - def get_environ(self): ... + def get_environ(self, sock): ... def makefile(self, sock, mode: str = ..., bufsize: int = ...): ... diff --git a/lib/cheroot/ssl/pyopenssl.py b/lib/cheroot/ssl/pyopenssl.py index adc9a1ba..548200f7 100644 --- a/lib/cheroot/ssl/pyopenssl.py +++ b/lib/cheroot/ssl/pyopenssl.py @@ -50,16 +50,11 @@ will be read, and the context will be automatically created from them. pyopenssl """ -from __future__ import absolute_import, division, print_function -__metaclass__ = type - import socket import sys import threading import time -import six - try: import OpenSSL.version from OpenSSL import SSL @@ -229,8 +224,7 @@ class SSLConnectionProxyMeta: return type(name, bases, nmspc) -@six.add_metaclass(SSLConnectionProxyMeta) -class SSLConnection: +class SSLConnection(metaclass=SSLConnectionProxyMeta): r"""A thread-safe wrapper for an ``SSL.Connection``. :param tuple args: the arguments to create the wrapped \ diff --git a/lib/cheroot/ssl/pyopenssl.pyi b/lib/cheroot/ssl/pyopenssl.pyi index d5b93471..107675c9 100644 --- a/lib/cheroot/ssl/pyopenssl.pyi +++ b/lib/cheroot/ssl/pyopenssl.pyi @@ -1,9 +1,9 @@ from . import Adapter from ..makefile import StreamReader, StreamWriter from OpenSSL import SSL -from typing import Any +from typing import Any, Type -ssl_conn_type: SSL.Connection +ssl_conn_type: Type[SSL.Connection] class SSLFileobjectMixin: ssl_timeout: int @@ -13,13 +13,13 @@ class SSLFileobjectMixin: def sendall(self, *args, **kwargs): ... def send(self, *args, **kwargs): ... -class SSLFileobjectStreamReader(SSLFileobjectMixin, StreamReader): ... # type:ignore -class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter): ... # type:ignore +class SSLFileobjectStreamReader(SSLFileobjectMixin, StreamReader): ... # type:ignore[misc] +class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter): ... # type:ignore[misc] class SSLConnectionProxyMeta: def __new__(mcl, name, bases, nmspc): ... -class SSLConnection(): +class SSLConnection: def __init__(self, *args) -> None: ... class pyOpenSSLAdapter(Adapter): @@ -28,3 +28,4 @@ class pyOpenSSLAdapter(Adapter): def wrap(self, sock): ... def get_environ(self): ... def makefile(self, sock, mode: str = ..., bufsize: int = ...): ... + def get_context(self) -> SSL.Context: ... diff --git a/lib/cheroot/test/_pytest_plugin.py b/lib/cheroot/test/_pytest_plugin.py index 012211df..8ff3b02c 100644 --- a/lib/cheroot/test/_pytest_plugin.py +++ b/lib/cheroot/test/_pytest_plugin.py @@ -8,6 +8,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type import pytest +import six pytest_version = tuple(map(int, pytest.__version__.split('.'))) @@ -43,8 +44,17 @@ def pytest_load_initial_conftests(early_config, parser, args): '= 2.15.4. + # Refs: + # * https://github.com/PyCQA/pylint/issues/6592 + # * https://github.com/PyCQA/pylint/pull/7395 + # pylint: disable-next=too-many-function-args _munge('/привіт'): hello, + # pylint: disable-next=too-many-function-args _munge('/Юххууу'): hello, '/\xa0Ðblah key 0 900 4 data': hello, '/*': asterisk, @@ -151,7 +149,6 @@ def test_parse_acceptable_uri(test_client, uri): assert actual_status == HTTP_OK -@pytest.mark.xfail(six.PY2, reason='Fails on Python 2') def test_parse_uri_unsafe_uri(test_client): """Test that malicious URI does not allow HTTP injection. @@ -263,6 +260,8 @@ def test_no_content_length(test_client): assert actual_status == HTTP_OK assert actual_resp_body == b'Hello world!' + c.close() # deal with the resource warning + def test_content_length_required(test_client): """Test POST query with body failing because of missing Content-Length.""" @@ -278,6 +277,8 @@ def test_content_length_required(test_client): actual_status = response.status assert actual_status == HTTP_LENGTH_REQUIRED + c.close() # deal with the resource warning + @pytest.mark.xfail( reason='https://github.com/cherrypy/cheroot/issues/106', @@ -350,6 +351,8 @@ def test_malformed_http_method(test_client): actual_resp_body = response.read(21) assert actual_resp_body == b'Malformed method name' + c.close() # deal with the resource warning + def test_malformed_header(test_client): """Check that broken HTTP header results in Bad Request.""" @@ -366,6 +369,8 @@ def test_malformed_header(test_client): actual_resp_body = response.read(20) assert actual_resp_body == b'Illegal header line.' + c.close() # deal with the resource warning + def test_request_line_split_issue_1220(test_client): """Check that HTTP request line of exactly 256 chars length is OK.""" diff --git a/lib/cheroot/test/test_dispatch.py b/lib/cheroot/test/test_dispatch.py index 9974fdab..c42014fa 100644 --- a/lib/cheroot/test/test_dispatch.py +++ b/lib/cheroot/test/test_dispatch.py @@ -1,8 +1,4 @@ """Tests for the HTTP server.""" -# -*- coding: utf-8 -*- -# vim: set fileencoding=utf-8 : - -from __future__ import absolute_import, division, print_function from cheroot.wsgi import PathInfoDispatcher diff --git a/lib/cheroot/test/test_makefile.py b/lib/cheroot/test/test_makefile.py index cdded07e..57f6f57e 100644 --- a/lib/cheroot/test/test_makefile.py +++ b/lib/cheroot/test/test_makefile.py @@ -3,9 +3,6 @@ from cheroot import makefile -__metaclass__ = type - - class MockSocket: """A mock socket.""" diff --git a/lib/cheroot/test/test_server.py b/lib/cheroot/test/test_server.py index 8305c78c..5e0a6832 100644 --- a/lib/cheroot/test/test_server.py +++ b/lib/cheroot/test/test_server.py @@ -1,23 +1,18 @@ """Tests for the HTTP server.""" -# -*- coding: utf-8 -*- -# vim: set fileencoding=utf-8 : - -from __future__ import absolute_import, division, print_function -__metaclass__ = type import os +import queue import socket import tempfile import threading import uuid +import urllib.parse # noqa: WPS301 import pytest import requests import requests_unixsocket -import six from pypytools.gc.custom import DefaultGc -from six.moves import queue, urllib from .._compat import bton, ntob from .._compat import IS_LINUX, IS_MACOS, IS_WINDOWS, SYS_PLATFORM @@ -259,12 +254,12 @@ def peercreds_enabled_server(http_server, unix_sock_file): @unix_only_sock_test @non_macos_sock_test -def test_peercreds_unix_sock(peercreds_enabled_server): +def test_peercreds_unix_sock(http_request_timeout, peercreds_enabled_server): """Check that ``PEERCRED`` lookup works when enabled.""" httpserver = peercreds_enabled_server bind_addr = httpserver.bind_addr - if isinstance(bind_addr, six.binary_type): + if isinstance(bind_addr, bytes): bind_addr = bind_addr.decode() # pylint: disable=possibly-unused-variable @@ -275,11 +270,17 @@ def test_peercreds_unix_sock(peercreds_enabled_server): expected_peercreds = '|'.join(map(str, expected_peercreds)) with requests_unixsocket.monkeypatch(): - peercreds_resp = requests.get(unix_base_uri + PEERCRED_IDS_URI) + peercreds_resp = requests.get( + unix_base_uri + PEERCRED_IDS_URI, + timeout=http_request_timeout, + ) peercreds_resp.raise_for_status() assert peercreds_resp.text == expected_peercreds - peercreds_text_resp = requests.get(unix_base_uri + PEERCRED_TEXTS_URI) + peercreds_text_resp = requests.get( + unix_base_uri + PEERCRED_TEXTS_URI, + timeout=http_request_timeout, + ) assert peercreds_text_resp.status_code == 500 @@ -290,14 +291,17 @@ def test_peercreds_unix_sock(peercreds_enabled_server): ) @unix_only_sock_test @non_macos_sock_test -def test_peercreds_unix_sock_with_lookup(peercreds_enabled_server): +def test_peercreds_unix_sock_with_lookup( + http_request_timeout, + peercreds_enabled_server, +): """Check that ``PEERCRED`` resolution works when enabled.""" httpserver = peercreds_enabled_server httpserver.peercreds_resolve_enabled = True bind_addr = httpserver.bind_addr - if isinstance(bind_addr, six.binary_type): + if isinstance(bind_addr, bytes): bind_addr = bind_addr.decode() # pylint: disable=possibly-unused-variable @@ -312,7 +316,10 @@ def test_peercreds_unix_sock_with_lookup(peercreds_enabled_server): ) expected_textcreds = '!'.join(map(str, expected_textcreds)) with requests_unixsocket.monkeypatch(): - peercreds_text_resp = requests.get(unix_base_uri + PEERCRED_TEXTS_URI) + peercreds_text_resp = requests.get( + unix_base_uri + PEERCRED_TEXTS_URI, + timeout=http_request_timeout, + ) peercreds_text_resp.raise_for_status() assert peercreds_text_resp.text == expected_textcreds @@ -363,7 +370,10 @@ def test_high_number_of_file_descriptors(native_server_client, resource_limit): assert any(fn >= resource_limit for fn in native_process_conn.filenos) -if not IS_WINDOWS: +ISSUE511 = IS_MACOS + + +if not IS_WINDOWS and not ISSUE511: test_high_number_of_file_descriptors = pytest.mark.forked( test_high_number_of_file_descriptors, ) diff --git a/lib/cheroot/test/test_ssl.py b/lib/cheroot/test/test_ssl.py index 8da330df..c55e156f 100644 --- a/lib/cheroot/test/test_ssl.py +++ b/lib/cheroot/test/test_ssl.py @@ -1,9 +1,4 @@ """Tests for TLS support.""" -# -*- coding: utf-8 -*- -# vim: set fileencoding=utf-8 : - -from __future__ import absolute_import, division, print_function -__metaclass__ = type import functools import json @@ -14,11 +9,11 @@ import sys import threading import time import traceback +import http.client import OpenSSL.SSL import pytest import requests -import six import trustme from .._compat import bton, ntob, ntou @@ -49,9 +44,6 @@ IS_PYOPENSSL_SSL_VERSION_1_0 = ( OpenSSL.SSL.SSLeay_version(OpenSSL.SSL.SSLEAY_VERSION). startswith(b'OpenSSL 1.0.') ) -PY27 = sys.version_info[:2] == (2, 7) -PY34 = sys.version_info[:2] == (3, 4) -PY3 = not six.PY2 PY310_PLUS = sys.version_info[:2] >= (3, 10) @@ -64,13 +56,12 @@ _stdlib_to_openssl_verify = { fails_under_py3 = pytest.mark.xfail( - not six.PY2, reason='Fails under Python 3+', ) fails_under_py3_in_pypy = pytest.mark.xfail( - not six.PY2 and IS_PYPY, + IS_PYPY, reason='Fails under PyPy3', ) @@ -213,6 +204,7 @@ def thread_exceptions(): ), ) def test_ssl_adapters( + http_request_timeout, tls_http_server, adapter_type, tls_certificate, tls_certificate_chain_pem_path, @@ -241,6 +233,7 @@ def test_ssl_adapters( resp = requests.get( 'https://{host!s}:{port!s}/'.format(host=interface, port=port), + timeout=http_request_timeout, verify=tls_ca_certificate_pem_path, ) @@ -276,8 +269,9 @@ def test_ssl_adapters( reason='Fails under PyPy in CI for unknown reason', strict=False, ) -def test_tls_client_auth( # noqa: C901 # FIXME +def test_tls_client_auth( # noqa: C901, WPS213 # FIXME # FIXME: remove twisted logic, separate tests + http_request_timeout, mocker, tls_http_server, adapter_type, ca, @@ -331,6 +325,9 @@ def test_tls_client_auth( # noqa: C901 # FIXME requests.get, 'https://{host!s}:{port!s}/'.format(host=interface, port=port), + # Don't wait for the first byte forever: + timeout=http_request_timeout, + # Server TLS certificate verification: verify=tls_ca_certificate_pem_path, @@ -348,12 +345,13 @@ def test_tls_client_auth( # noqa: C901 # FIXME and tls_verify_mode == ssl.CERT_REQUIRED and tls_client_identity == 'localhost' and is_trusted_cert - ) or PY34: + ): pytest.xfail( 'OpenSSL 1.0 has problems with verifying client certs', ) assert is_req_successful assert resp.text == 'Hello world!' + resp.close() return # xfail some flaky tests @@ -366,29 +364,16 @@ def test_tls_client_auth( # noqa: C901 # FIXME if issue_237: pytest.xfail('Test sometimes fails') - expected_ssl_errors = ( - requests.exceptions.SSLError, - OpenSSL.SSL.Error, - ) if PY34 else ( - requests.exceptions.SSLError, - ) + expected_ssl_errors = requests.exceptions.SSLError, if IS_WINDOWS or IS_GITHUB_ACTIONS_WORKFLOW: expected_ssl_errors += requests.exceptions.ConnectionError, with pytest.raises(expected_ssl_errors) as ssl_err: - make_https_request() - - if PY34 and isinstance(ssl_err, OpenSSL.SSL.Error): - pytest.xfail( - 'OpenSSL behaves wierdly under Python 3.4 ' - 'because of an outdated urllib3', - ) + make_https_request().close() try: err_text = ssl_err.value.args[0].reason.args[0].args[0] except AttributeError: - if PY34: - pytest.xfail('OpenSSL behaves wierdly under Python 3.4') - elif IS_WINDOWS or IS_GITHUB_ACTIONS_WORKFLOW: + if IS_WINDOWS or IS_GITHUB_ACTIONS_WORKFLOW: err_text = str(ssl_err.value) else: raise @@ -400,9 +385,8 @@ def test_tls_client_auth( # noqa: C901 # FIXME 'sslv3 alert bad certificate' if IS_LIBRESSL_BACKEND else 'tlsv1 alert unknown ca', ) - if not six.PY2: - if IS_MACOS and IS_PYPY and adapter_type == 'pyopenssl': - expected_substrings = ('tlsv1 alert unknown ca',) + if IS_MACOS and IS_PYPY and adapter_type == 'pyopenssl': + expected_substrings = ('tlsv1 alert unknown ca',) if ( tls_verify_mode in ( ssl.CERT_REQUIRED, @@ -469,9 +453,9 @@ def test_tls_client_auth( # noqa: C901 # FIXME pytest.param( 'builtin', marks=pytest.mark.xfail( - IS_GITHUB_ACTIONS_WORKFLOW and IS_MACOS and PY310_PLUS, + IS_MACOS and PY310_PLUS, reason='Unclosed TLS resource warnings happen on macOS ' - 'under Python 3.10', + 'under Python 3.10 (#508)', strict=False, ), ), @@ -492,6 +476,7 @@ def test_ssl_env( # noqa: C901 # FIXME thread_exceptions, recwarn, mocker, + http_request_timeout, tls_http_server, adapter_type, ca, tls_verify_mode, tls_certificate, tls_certificate_chain_pem_path, @@ -532,13 +517,10 @@ def test_ssl_env( # noqa: C901 # FIXME resp = requests.get( 'https://' + interface + ':' + str(port) + '/env', + timeout=http_request_timeout, verify=tls_ca_certificate_pem_path, cert=cl_pem if use_client_cert else None, ) - if PY34 and resp.status_code != 200: - pytest.xfail( - 'Python 3.4 has problems with verifying client certs', - ) env = json.loads(resp.content.decode('utf-8')) @@ -620,7 +602,7 @@ def test_https_over_http_error(http_server, ip_addr): httpserver = http_server.send((ip_addr, EPHEMERAL_PORT)) interface, _host, port = _get_conn_data(httpserver.bind_addr) with pytest.raises(ssl.SSLError) as ssl_err: - six.moves.http_client.HTTPSConnection( + http.client.HTTPSConnection( '{interface}:{port}'.format( interface=interface, port=port, @@ -633,20 +615,10 @@ def test_https_over_http_error(http_server, ip_addr): assert expected_substring in ssl_err.value.args[-1] -http_over_https_error_builtin_marks = [] -if IS_WINDOWS and six.PY2: - http_over_https_error_builtin_marks.append( - pytest.mark.flaky(reruns=5, reruns_delay=2), - ) - - @pytest.mark.parametrize( 'adapter_type', ( - pytest.param( - 'builtin', - marks=http_over_https_error_builtin_marks, - ), + 'builtin', 'pyopenssl', ), ) @@ -657,7 +629,9 @@ if IS_WINDOWS and six.PY2: pytest.param(ANY_INTERFACE_IPV6, marks=missing_ipv6), ), ) +@pytest.mark.flaky(reruns=3, reruns_delay=2) def test_http_over_https_error( + http_request_timeout, tls_http_server, adapter_type, ca, ip_addr, tls_certificate, @@ -697,36 +671,12 @@ def test_http_over_https_error( expect_fallback_response_over_plain_http = ( ( adapter_type == 'pyopenssl' - and (IS_ABOVE_OPENSSL10 or not six.PY2) ) - or PY27 - ) or ( - IS_GITHUB_ACTIONS_WORKFLOW - and IS_WINDOWS - and six.PY2 - and not IS_WIN2016 ) - if ( - IS_GITHUB_ACTIONS_WORKFLOW - and IS_WINDOWS - and six.PY2 - and IS_WIN2016 - and adapter_type == 'builtin' - and ip_addr is ANY_INTERFACE_IPV6 - ): - expect_fallback_response_over_plain_http = True - if ( - IS_GITHUB_ACTIONS_WORKFLOW - and IS_WINDOWS - and six.PY2 - and not IS_WIN2016 - and adapter_type == 'builtin' - and ip_addr is not ANY_INTERFACE_IPV6 - ): - expect_fallback_response_over_plain_http = False if expect_fallback_response_over_plain_http: resp = requests.get( 'http://{host!s}:{port!s}/'.format(host=fqdn, port=port), + timeout=http_request_timeout, ) assert resp.status_code == 400 assert resp.text == ( @@ -738,6 +688,7 @@ def test_http_over_https_error( with pytest.raises(requests.exceptions.ConnectionError) as ssl_err: requests.get( # FIXME: make stdlib ssl behave like PyOpenSSL 'http://{host!s}:{port!s}/'.format(host=fqdn, port=port), + timeout=http_request_timeout, ) if IS_LINUX: diff --git a/lib/cheroot/test/test_wsgi.py b/lib/cheroot/test/test_wsgi.py index 91dfb71e..14005a84 100644 --- a/lib/cheroot/test/test_wsgi.py +++ b/lib/cheroot/test/test_wsgi.py @@ -37,6 +37,7 @@ def simple_wsgi_server(): yield locals() +@pytest.mark.flaky(reruns=3, reruns_delay=2) def test_connection_keepalive(simple_wsgi_server): """Test the connection keepalive works (duh).""" session = Session(base_url=simple_wsgi_server['url']) @@ -59,6 +60,7 @@ def test_connection_keepalive(simple_wsgi_server): ] failures = sum(task.result() for task in tasks) + session.close() assert not failures diff --git a/lib/cheroot/test/webtest.py b/lib/cheroot/test/webtest.py index 118014a6..1630c8ef 100644 --- a/lib/cheroot/test/webtest.py +++ b/lib/cheroot/test/webtest.py @@ -15,9 +15,6 @@ the traceback to stdout, and keep any assertions you have from running be of further significance to your tests). """ -from __future__ import absolute_import, division, print_function -__metaclass__ = type - import pprint import re import socket @@ -29,9 +26,8 @@ import json import unittest # pylint: disable=deprecated-module,preferred-module import warnings import functools - -from six.moves import http_client, map, urllib_parse -import six +import http.client +import urllib.parse from more_itertools.more import always_iterable import jaraco.functools @@ -105,7 +101,7 @@ class WebCase(unittest.TestCase): HOST = '127.0.0.1' PORT = 8000 - HTTP_CONN = http_client.HTTPConnection + HTTP_CONN = http.client.HTTPConnection PROTOCOL = 'HTTP/1.1' scheme = 'http' @@ -127,7 +123,7 @@ class WebCase(unittest.TestCase): * from :py:mod:`python:http.client`. """ cls_name = '{scheme}Connection'.format(scheme=self.scheme.upper()) - return getattr(http_client, cls_name) + return getattr(http.client, cls_name) def get_conn(self, auto_open=False): """Return a connection to our HTTP server.""" @@ -201,9 +197,9 @@ class WebCase(unittest.TestCase): """ ServerError.on = False - if isinstance(url, six.text_type): + if isinstance(url, str): url = url.encode('utf-8') - if isinstance(body, six.text_type): + if isinstance(body, str): body = body.encode('utf-8') # for compatibility, support raise_subcls is None @@ -386,7 +382,7 @@ class WebCase(unittest.TestCase): def assertBody(self, value, msg=None): """Fail if value != self.body.""" - if isinstance(value, six.text_type): + if isinstance(value, str): value = value.encode(self.encoding) if value != self.body: if msg is None: @@ -397,7 +393,7 @@ class WebCase(unittest.TestCase): def assertInBody(self, value, msg=None): """Fail if value not in self.body.""" - if isinstance(value, six.text_type): + if isinstance(value, str): value = value.encode(self.encoding) if value not in self.body: if msg is None: @@ -406,7 +402,7 @@ class WebCase(unittest.TestCase): def assertNotInBody(self, value, msg=None): """Fail if value in self.body.""" - if isinstance(value, six.text_type): + if isinstance(value, str): value = value.encode(self.encoding) if value in self.body: if msg is None: @@ -415,7 +411,7 @@ class WebCase(unittest.TestCase): def assertMatchesBody(self, pattern, msg=None, flags=0): """Fail if value (a regex pattern) is not in self.body.""" - if isinstance(pattern, six.text_type): + if isinstance(pattern, str): pattern = pattern.encode(self.encoding) if re.search(pattern, self.body, flags) is None: if msg is None: @@ -464,25 +460,7 @@ def shb(response): """Return status, headers, body the way we like from a response.""" resp_status_line = '%s %s' % (response.status, response.reason) - if not six.PY2: - return resp_status_line, response.getheaders(), response.read() - - h = [] - key, value = None, None - for line in response.msg.headers: - if line: - if line[0] in ' \t': - value += line.strip() - else: - if key and value: - h.append((key, value)) - key, value = line.split(':', 1) - key = key.strip() - value = value.strip() - if key and value: - h.append((key, value)) - - return resp_status_line, h, response.read() + return resp_status_line, response.getheaders(), response.read() # def openURL(*args, raise_subcls=(), **kwargs): @@ -514,7 +492,7 @@ def openURL(*args, **kwargs): def _open_url_once( url, headers=None, method='GET', body=None, - host='127.0.0.1', port=8000, http_conn=http_client.HTTPConnection, + host='127.0.0.1', port=8000, http_conn=http.client.HTTPConnection, protocol='HTTP/1.1', ssl_context=None, ): """Open the given HTTP resource and return status, headers, and body.""" @@ -530,7 +508,7 @@ def _open_url_once( conn = http_conn(interface(host), port, **kw) conn._http_vsn_str = protocol conn._http_vsn = int(''.join([x for x in protocol if x.isdigit()])) - if not six.PY2 and isinstance(url, bytes): + if isinstance(url, bytes): url = url.decode() conn.putrequest( method.upper(), url, skip_host=True, @@ -572,10 +550,10 @@ def strip_netloc(url): >>> strip_netloc('/foo/bar?bing#baz') '/foo/bar?bing' """ - parsed = urllib_parse.urlparse(url) + parsed = urllib.parse.urlparse(url) _scheme, _netloc, path, params, query, _fragment = parsed stripped = '', '', path, params, query, '' - return urllib_parse.urlunparse(stripped) + return urllib.parse.urlunparse(stripped) # Add any exceptions which your web framework handles diff --git a/lib/cheroot/testing.py b/lib/cheroot/testing.py index c9a6ac99..169142bf 100644 --- a/lib/cheroot/testing.py +++ b/lib/cheroot/testing.py @@ -1,16 +1,13 @@ """Pytest fixtures and other helpers for doing testing by end-users.""" -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -from contextlib import closing +from contextlib import closing, contextmanager import errno import socket import threading import time +import http.client import pytest -from six.moves import http_client import cheroot.server from cheroot.test import webtest @@ -33,6 +30,7 @@ config = { } +@contextmanager def cheroot_server(server_factory): """Set up and tear down a Cheroot server instance.""" conf = config[server_factory].copy() @@ -64,14 +62,14 @@ def cheroot_server(server_factory): @pytest.fixture def wsgi_server(): """Set up and tear down a Cheroot WSGI server instance.""" - for srv in cheroot_server(cheroot.wsgi.Server): + with cheroot_server(cheroot.wsgi.Server) as srv: yield srv @pytest.fixture def native_server(): """Set up and tear down a Cheroot HTTP server instance.""" - for srv in cheroot_server(cheroot.server.HTTPServer): + with cheroot_server(cheroot.server.HTTPServer) as srv: yield srv @@ -89,9 +87,9 @@ class _TestClient: port=self._port, ) conn_cls = ( - http_client.HTTPConnection + http.client.HTTPConnection if self.server_instance.ssl_adapter is None else - http_client.HTTPSConnection + http.client.HTTPSConnection ) return conn_cls(name) diff --git a/lib/cheroot/workers/threadpool.py b/lib/cheroot/workers/threadpool.py index 795ebc6d..2a9878dc 100644 --- a/lib/cheroot/workers/threadpool.py +++ b/lib/cheroot/workers/threadpool.py @@ -5,17 +5,12 @@ joinable """ -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - import collections import threading import time import socket import warnings - -from six.moves import queue +import queue from jaraco.functools import pass_none @@ -178,7 +173,7 @@ class ThreadPool: for worker in self._threads: worker.name = ( 'CP Server {worker_name!s}'. - format(worker_name=worker.name), + format(worker_name=worker.name) ) worker.start() for worker in self._threads: @@ -228,7 +223,7 @@ class ThreadPool: worker = WorkerThread(self.server) worker.name = ( 'CP Server {worker_name!s}'. - format(worker_name=worker.name), + format(worker_name=worker.name) ) worker.start() return worker diff --git a/lib/cheroot/wsgi.py b/lib/cheroot/wsgi.py index 583d52a9..82faca3e 100644 --- a/lib/cheroot/wsgi.py +++ b/lib/cheroot/wsgi.py @@ -25,14 +25,8 @@ as you want in one instance by using a PathInfoDispatcher:: server = wsgi.Server(addr, d) """ -from __future__ import absolute_import, division, print_function -__metaclass__ = type - import sys -import six -from six.moves import filter - from . import server from .workers import threadpool from ._compat import ntob, bton @@ -140,7 +134,7 @@ class Gateway(server.Gateway): response = self.req.server.wsgi_app(self.env, self.start_response) try: for chunk in filter(None, response): - if not isinstance(chunk, six.binary_type): + if not isinstance(chunk, bytes): raise ValueError('WSGI Applications must yield bytes') self.write(chunk) finally: @@ -149,7 +143,7 @@ class Gateway(server.Gateway): if hasattr(response, 'close'): response.close() - def start_response(self, status, headers, exc_info=None): + def start_response(self, status, headers, exc_info=None): # noqa: WPS238 """WSGI callable to begin the HTTP response.""" # "The application may call start_response more than once, # if and only if the exc_info argument is provided." @@ -164,10 +158,8 @@ class Gateway(server.Gateway): # sent, start_response must raise an error, and should raise the # exc_info tuple." if self.req.sent_headers: - try: - six.reraise(*exc_info) - finally: - exc_info = None + value = exc_info[1] + raise value self.req.status = self._encode_status(status) @@ -196,8 +188,6 @@ class Gateway(server.Gateway): must be of type "str" but are restricted to code points in the "Latin-1" set. """ - if six.PY2: - return status if not isinstance(status, str): raise TypeError('WSGI response status is not of type str.') return status.encode('ISO-8859-1') @@ -273,7 +263,7 @@ class Gateway_10(Gateway): 'wsgi.version': self.version, } - if isinstance(req.server.bind_addr, six.string_types): + if isinstance(req.server.bind_addr, str): # AF_UNIX. This isn't really allowed by WSGI, which doesn't # address unix domain sockets. But it's better than nothing. env['SERVER_PORT'] = '' @@ -332,10 +322,10 @@ class Gateway_u0(Gateway_10): """Return a new environ dict targeting the given wsgi.version.""" req = self.req env_10 = super(Gateway_u0, self).get_environ() - env = dict(map(self._decode_key, env_10.items())) + env = dict(env_10.items()) # Request-URI - enc = env.setdefault(six.u('wsgi.url_encoding'), six.u('utf-8')) + enc = env.setdefault('wsgi.url_encoding', 'utf-8') try: env['PATH_INFO'] = req.path.decode(enc) env['QUERY_STRING'] = req.qs.decode(enc) @@ -345,25 +335,10 @@ class Gateway_u0(Gateway_10): env['PATH_INFO'] = env_10['PATH_INFO'] env['QUERY_STRING'] = env_10['QUERY_STRING'] - env.update(map(self._decode_value, env.items())) + env.update(env.items()) return env - @staticmethod - def _decode_key(item): - k, v = item - if six.PY2: - k = k.decode('ISO-8859-1') - return k, v - - @staticmethod - def _decode_value(item): - k, v = item - skip_keys = 'REQUEST_URI', 'wsgi.input' - if not six.PY2 or not isinstance(v, bytes) or k in skip_keys: - return k, v - return k, v.decode('ISO-8859-1') - wsgi_gateways = Gateway.gateway_map() diff --git a/lib/cheroot/wsgi.pyi b/lib/cheroot/wsgi.pyi index b4851a3d..96075633 100644 --- a/lib/cheroot/wsgi.pyi +++ b/lib/cheroot/wsgi.pyi @@ -40,3 +40,10 @@ class PathInfoDispatcher: apps: Any def __init__(self, apps): ... def __call__(self, environ, start_response): ... + + +WSGIServer = Server +WSGIGateway = Gateway +WSGIGateway_u0 = Gateway_u0 +WSGIGateway_10 = Gateway_10 +WSGIPathInfoDispatcher = PathInfoDispatcher diff --git a/requirements.txt b/requirements.txt index 2a5b1836..dfafa748 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ backports.zoneinfo==0.2.1 beautifulsoup4==4.11.1 bleach==5.0.1 certifi==2022.9.24 -cheroot==8.6.0 +cheroot==9.0.0 cherrypy==18.8.0 cloudinary==1.30.0 distro==1.8.0