Hello community, here is the log from the commit of package python-cheroot for openSUSE:Factory checked in at 2019-11-04 17:08:30 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-cheroot (Old) and /work/SRC/openSUSE:Factory/.python-cheroot.new.2990 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-cheroot" Mon Nov 4 17:08:30 2019 rev:9 rq:742156 version:8.2.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-cheroot/python-cheroot.changes 2019-10-09 15:17:45.416761145 +0200 +++ /work/SRC/openSUSE:Factory/.python-cheroot.new.2990/python-cheroot.changes 2019-11-04 17:08:31.476396904 +0100 @@ -1,0 +2,11 @@ +Wed Oct 23 13:38:06 UTC 2019 - Marketa Calabkova <mcalabk...@suse.com> + +- Update to 8.2.1 + * Deprecated use of negative timeouts as alias for infinite timeouts in ThreadPool.stop. + * For OPTION requests, bypass URI as path if it does not appear absolute. + * Workers are now request-based, addressing the long-standing issue with keep-alive connections + * Remove custom setup.cfg parser handling, allowing the project (including sdist) + to build/run on setuptools 41.4. Now building cheroot requires setuptools 30.3 or later + (for declarative config support) and preferably 34.4 or later (as indicated in pyproject.toml). + +------------------------------------------------------------------- Old: ---- cheroot-7.0.0.tar.gz New: ---- cheroot-8.2.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-cheroot.spec ++++++ --- /var/tmp/diff_new_pack.9frw3c/_old 2019-11-04 17:08:32.068397536 +0100 +++ /var/tmp/diff_new_pack.9frw3c/_new 2019-11-04 17:08:32.068397536 +0100 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} %define pypi_name cheroot Name: python-%{pypi_name} -Version: 7.0.0 +Version: 8.2.1 Release: 0 Summary: Pure-python HTTP server License: BSD-3-Clause @@ -32,9 +32,9 @@ BuildRequires: %{python_module pytest-mock >= 1.10.4} BuildRequires: %{python_module requests-unixsocket} BuildRequires: %{python_module requests} +BuildRequires: %{python_module setuptools >= 34.4} BuildRequires: %{python_module setuptools_scm >= 1.15.0} BuildRequires: %{python_module setuptools_scm_git_archive >= 1.0} -BuildRequires: %{python_module setuptools} BuildRequires: %{python_module six >= 1.11.0} BuildRequires: %{python_module trustme} BuildRequires: fdupes @@ -63,10 +63,6 @@ %autosetup -n cheroot-%{version} -p1 # do not require cov/xdist/etc sed -i -e '/addopts/d' pytest.ini -for file in "%{pypi_name}.egg-info/requires.txt" "setup.cfg"; do - sed -i "s/backports.functools_lru_cache$/backports.functools_lru_cache ; python_version < '3.3'/" \ - "${file}" -done %build %python_build ++++++ cheroot-7.0.0.tar.gz -> cheroot-8.2.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/.circleci/config.yml new/cheroot-8.2.1/.circleci/config.yml --- old/cheroot-7.0.0/.circleci/config.yml 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/.circleci/config.yml 2019-10-18 02:59:30.000000000 +0200 @@ -35,7 +35,7 @@ - checkout - run: name: Run tests - command: tox -e py27,py34,py35,py36,py37,pypy3 -- -p no:sugar + command: tox -e py27,py34,py35,py36,py37,pypy3 -- -p no:sugar $(circleci tests glob **/test/**.py | circleci tests split --split-by=timings | grep -v '__init__.py') # Environment variables for py-cryptography library environment: LDFLAGS: "-L/usr/local/opt/openssl/lib" @@ -51,7 +51,7 @@ steps: - checkout - run: pip install tox - - run: tox -e py27,py34,py35,py36,py37 + - run: tox -e py27,py34,py35,py36,py37 -- $(circleci tests glob **/test/**.py | circleci tests split --split-by=timings | grep -v '__init__.py') workflows: version: 2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/.github/FUNDING.yml new/cheroot-8.2.1/.github/FUNDING.yml --- old/cheroot-7.0.0/.github/FUNDING.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/cheroot-8.2.1/.github/FUNDING.yml 2019-10-18 02:59:30.000000000 +0200 @@ -0,0 +1,14 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +- jaraco +- webknjaz +# patreon: # Replace with a single Patreon username +# open_collective: # Replace with a single Open Collective username +# ko_fi: # Replace with a single Ko-fi username +tidelift: pypi/Cheroot # A single Tidelift platform-name/package-name e.g., npm/babel +# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +# liberapay: # Replace with a single Liberapay username +# issuehunt: # Replace with a single IssueHunt username +# otechie: # Replace with a single Otechie username +# custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/.github/workflows/python-tests.yml new/cheroot-8.2.1/.github/workflows/python-tests.yml --- old/cheroot-7.0.0/.github/workflows/python-tests.yml 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/.github/workflows/python-tests.yml 2019-10-18 02:59:30.000000000 +0200 @@ -49,10 +49,8 @@ + repr(ssl.OPENSSL_VERSION_NUMBER))" env: ${{ matrix.env }} - name: Log PyOpenSSL version - run: >- - python -m tox --run-command - "{envpython} -m OpenSSL.debug" - || : + run: | + python -m tox -e openssl-version env: ${{ matrix.env }} - name: Test with tox run: | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/.pyup.yml new/cheroot-8.2.1/.pyup.yml --- old/cheroot-7.0.0/.pyup.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/cheroot-8.2.1/.pyup.yml 2019-10-18 02:59:30.000000000 +0200 @@ -0,0 +1 @@ +pin: False diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/CHANGES.rst new/cheroot-8.2.1/CHANGES.rst --- old/cheroot-7.0.0/CHANGES.rst 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/CHANGES.rst 2019-10-18 02:59:30.000000000 +0200 @@ -1,3 +1,34 @@ +v8.2.1 +====== + +- :cp-issue:`1818`: Restore support for ``None`` + default argument to ``WebCase.getPage()``. + +v8.2.0 +====== + +- Deprecated use of negative timeouts as alias for + infinite timeouts in ``ThreadPool.stop``. +- :cp-issue:`1662` via :pr:`74`: For OPTION requests, + bypass URI as path if it does not appear absolute. + +v8.1.0 +====== + +- Workers are now request-based, addressing the + long-standing issue with keep-alive connections + (:issue:`91` via :pr:`199`). + +v8.0.0 +====== + +- :issue:`231` via :pr:`232`: Remove custom setup.cfg + parser handling, allowing the project (including sdist) + to build/run on setuptools 41.4. Now building cheroot + requires setuptools 30.3 or later (for declarative + config support) and preferably 34.4 or later (as + indicated in pyproject.toml). + v7.0.0 ====== diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/PKG-INFO new/cheroot-8.2.1/PKG-INFO --- old/cheroot-7.0.0/PKG-INFO 2019-09-26 22:59:42.000000000 +0200 +++ new/cheroot-8.2.1/PKG-INFO 2019-10-18 02:59:51.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: cheroot -Version: 7.0.0 +Version: 8.2.1 Summary: Highly-optimized, pure-python HTTP server Home-page: https://cheroot.cherrypy.org Author: CherryPy Team @@ -102,6 +102,6 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server -Requires-Python: >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* -Provides-Extra: testing +Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7 Provides-Extra: docs +Provides-Extra: testing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot/connections.py new/cheroot-8.2.1/cheroot/connections.py --- old/cheroot-7.0.0/cheroot/connections.py 1970-01-01 01:00:00.000000000 +0100 +++ new/cheroot-8.2.1/cheroot/connections.py 2019-10-18 02:59:30.000000000 +0200 @@ -0,0 +1,279 @@ +"""Utilities to manage open connections.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import io +import os +import select +import socket +import time + +from . import errors +from .makefile import MakeFile + +import six + +try: + import fcntl +except ImportError: + try: + from ctypes import windll, WinError + import ctypes.wintypes + _SetHandleInformation = windll.kernel32.SetHandleInformation + _SetHandleInformation.argtypes = [ + ctypes.wintypes.HANDLE, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ] + _SetHandleInformation.restype = ctypes.wintypes.BOOL + except ImportError: + def prevent_socket_inheritance(sock): + """Stub inheritance prevention. + + Dummy function, since neither fcntl nor ctypes are available. + """ + pass + else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (Windows).""" + if not _SetHandleInformation(sock.fileno(), 1, 0): + raise WinError() +else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (POSIX).""" + fd = sock.fileno() + old_flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) + + +class ConnectionManager: + """Class which manages HTTPConnection objects. + + This is for connections which are being kept-alive for follow-up requests. + """ + + def __init__(self, server): + """Initialize ConnectionManager object. + + Args: + server (cheroot.server.HTTPServer): web server object + that uses this ConnectionManager instance. + """ + self.server = server + self.connections = [] + + def put(self, conn): + """Put idle connection into the ConnectionManager to be managed. + + Args: + conn (cheroot.server.HTTPConnection): HTTP connection + to be managed. + """ + conn.last_used = time.time() + conn.ready_with_data = conn.rfile.has_data() + self.connections.append(conn) + + def expire(self): + """Expire least recently used connections. + + This happens if there are either too many open connections, or if the + connections have been timed out. + + This should be called periodically. + """ + if not self.connections: + return + + # Look at the first connection - if it can be closed, then do + # that, and wait for get_conn to return it. + conn = self.connections[0] + if conn.closeable: + return + + # Too many connections? + ka_limit = self.server.keep_alive_conn_limit + if ka_limit is not None and len(self.connections) > ka_limit: + conn.closeable = True + return + + # Connection too old? + if (conn.last_used + self.server.timeout) < time.time(): + conn.closeable = True + return + + def get_conn(self, server_socket): + """Return a HTTPConnection object which is ready to be handled. + + A connection returned by this method should be ready for a worker + to handle it. If there are no connections ready, None will be + returned. + + Any connection returned by this method will need to be `put` + back if it should be examined again for another request. + + Args: + server_socket (socket.socket): Socket to listen to for new + connections. + Returns: + cheroot.server.HTTPConnection instance, or None. + + """ + # Grab file descriptors from sockets, but stop if we find a + # connection which is already marked as ready. + socket_dict = {} + for conn in self.connections: + if conn.closeable or conn.ready_with_data: + break + socket_dict[conn.socket.fileno()] = conn + else: + # No ready connection. + conn = None + + # We have a connection ready for use. + if conn: + self.connections.remove(conn) + return conn + + # Will require a select call. + ss_fileno = server_socket.fileno() + socket_dict[ss_fileno] = server_socket + try: + rlist, _, _ = select.select(list(socket_dict), [], [], 0.1) + # No available socket. + if not rlist: + return None + except OSError: + # Mark any connection which no longer appears valid. + for fno, conn in list(socket_dict.items()): + # If the server socket is invalid, we'll just ignore it and + # wait to be shutdown. + if fno == ss_fileno: + continue + try: + os.fstat(fno) + except OSError: + # Socket is invalid, close the connection, insert at + # the front. + self.connections.remove(conn) + self.connections.insert(0, conn) + conn.closeable = True + + # Wait for the next tick to occur. + return None + + try: + # See if we have a new connection coming in. + rlist.remove(ss_fileno) + except ValueError: + # No new connection, but reuse existing socket. + conn = socket_dict[rlist.pop()] + else: + conn = server_socket + + # All remaining connections in rlist should be marked as ready. + for fno in rlist: + socket_dict[fno].ready_with_data = True + + # New connection. + if conn is server_socket: + return self._from_server_socket(server_socket) + + self.connections.remove(conn) + return conn + + def _from_server_socket(self, server_socket): + try: + s, addr = server_socket.accept() + if self.server.stats['Enabled']: + self.server.stats['Accepts'] += 1 + prevent_socket_inheritance(s) + if hasattr(s, 'settimeout'): + s.settimeout(self.server.timeout) + + mf = MakeFile + ssl_env = {} + # if ssl cert and key are set, we try to be a secure HTTP server + if self.server.ssl_adapter is not None: + try: + s, ssl_env = self.server.ssl_adapter.wrap(s) + except errors.NoSSLError: + msg = ( + 'The client sent a plain HTTP request, but ' + 'this server only speaks HTTPS on this port.' + ) + buf = [ + '%s 400 Bad Request\r\n' % self.server.protocol, + 'Content-Length: %s\r\n' % len(msg), + 'Content-Type: text/plain\r\n\r\n', + msg, + ] + + sock_to_make = s if not six.PY2 else s._sock + wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE) + try: + wfile.write(''.join(buf).encode('ISO-8859-1')) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + return + if not s: + return + mf = self.server.ssl_adapter.makefile + # Re-apply our timeout since we may have a new socket object + if hasattr(s, 'settimeout'): + s.settimeout(self.server.timeout) + + conn = self.server.ConnectionClass(self.server, s, mf) + + if not isinstance( + self.server.bind_addr, + (six.text_type, six.binary_type), + ): + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + if addr is None: # sometimes this can happen + # figure out if AF_INET or AF_INET6. + if len(s.getsockname()) == 2: + # AF_INET + addr = ('0.0.0.0', 0) + else: + # AF_INET6 + addr = ('::', 0) + conn.remote_addr = addr[0] + conn.remote_port = addr[1] + + conn.ssl_env = ssl_env + return conn + + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + return + except socket.error as ex: + if self.server.stats['Enabled']: + self.server.stats['Socket Errors'] += 1 + if ex.args[0] in errors.socket_error_eintr: + # I *think* this is right. EINTR should occur when a signal + # is received during the accept() call; all docs say retry + # the call, and I *think* I'm reading it right that Python + # will then go ahead and poll for and handle the signal + # elsewhere. See + # https://github.com/cherrypy/cherrypy/issues/707. + return + if ex.args[0] in errors.socket_errors_nonblocking: + # Just try again. See + # https://github.com/cherrypy/cherrypy/issues/479. + return + if ex.args[0] in errors.socket_errors_to_ignore: + # Our socket was closed. + # See https://github.com/cherrypy/cherrypy/issues/686. + return + raise + + def close(self): + """Close all monitored connections.""" + for conn in self.connections[:]: + conn.close() + self.connections = [] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot/makefile.py new/cheroot-8.2.1/cheroot/makefile.py --- old/cheroot-7.0.0/cheroot/makefile.py 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/cheroot/makefile.py 2019-10-18 02:59:30.000000000 +0200 @@ -235,6 +235,7 @@ break buf.write(data) return buf.getvalue() + else: # Read until size bytes or \n or EOF seen, whichever comes # first @@ -279,6 +280,11 @@ 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.""" @@ -395,6 +401,10 @@ buf_len += n return ''.join(buffers) + def has_data(self): + """Return true if there is buffered data to read.""" + return bool(self._rbuf) + if not six.PY2: class StreamReader(io.BufferedReader): @@ -411,6 +421,10 @@ 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.""" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot/server.py new/cheroot-8.2.1/cheroot/server.py --- old/cheroot-7.0.0/cheroot/server.py 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/cheroot/server.py 2019-10-18 02:59:30.000000000 +0200 @@ -84,7 +84,7 @@ from six.moves import queue from six.moves import urllib -from . import errors, __version__ +from . import connections, errors, __version__ from ._compat import bton, ntou from ._compat import IS_PPC from .workers import threadpool @@ -827,12 +827,14 @@ self.simple_response('400 Bad Request', 'Malformed Request-URI') return False + uri_is_absolute_form = (scheme or authority) + if self.method == b'OPTIONS': # TODO: cover this branch with tests path = ( uri # https://tools.ietf.org/html/rfc7230#section-5.3.4 - if self.proxy_mode or uri == ASTERISK + if (self.proxy_mode and uri_is_absolute_form) else path ) elif self.method == b'CONNECT': @@ -871,8 +873,6 @@ authority = path = _authority scheme = qs = fragment = EMPTY else: - uri_is_absolute_form = (scheme or authority) - disallowed_absolute = ( self.strict_mode and not self.proxy_mode @@ -1227,6 +1227,11 @@ peercreds_enabled = False 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. @@ -1255,30 +1260,26 @@ ) def communicate(self): - """Read each request and respond appropriately.""" + """Read each request and respond appropriately. + + Returns true if the connection should be kept open. + """ request_seen = False try: - while True: - # (re)set req to None so that if something goes wrong in - # the RequestHandlerClass constructor, the error doesn't - # get written to the previous request. - req = None - req = self.RequestHandlerClass(self.server, self) - - # This order of operations should guarantee correct pipelining. - req.parse_request() - if self.server.stats['Enabled']: - self.requests_seen += 1 - if not req.ready: - # Something went wrong in the parsing (and the server has - # probably already made a simple_response). Return and - # let the conn close. - return - - request_seen = True - req.respond() - if req.close_connection: - return + req = self.RequestHandlerClass(self.server, self) + req.parse_request() + if self.server.stats['Enabled']: + self.requests_seen += 1 + if not req.ready: + # Something went wrong in the parsing (and the server has + # probably already made a simple_response). Return and + # let the conn close. + return False + + request_seen = True + req.respond() + if not req.close_connection: + return True except socket.error as ex: errnum = ex.args[0] # sadly SSL sockets return a different (longer) time out string @@ -1307,6 +1308,7 @@ repr(ex), level=logging.ERROR, traceback=True, ) self._conditional_error(req, '500 Internal Server Error') + return False linger = False @@ -1474,39 +1476,6 @@ self.socket._sock.close() -try: - import fcntl -except ImportError: - try: - from ctypes import windll, WinError - import ctypes.wintypes - _SetHandleInformation = windll.kernel32.SetHandleInformation - _SetHandleInformation.argtypes = [ - ctypes.wintypes.HANDLE, - ctypes.wintypes.DWORD, - ctypes.wintypes.DWORD, - ] - _SetHandleInformation.restype = ctypes.wintypes.BOOL - except ImportError: - def prevent_socket_inheritance(sock): - """Stub inheritance prevention. - - Dummy function, since neither fcntl nor ctypes are available. - """ - pass - else: - def prevent_socket_inheritance(sock): - """Mark the given socket fd as non-inheritable (Windows).""" - if not _SetHandleInformation(sock.fileno(), 1, 0): - raise WinError() -else: - def prevent_socket_inheritance(sock): - """Mark the given socket fd as non-inheritable (POSIX).""" - fd = sock.fileno() - old_flags = fcntl.fcntl(fd, fcntl.F_GETFD) - fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) - - class HTTPServer: """An HTTP server.""" @@ -1582,6 +1551,11 @@ peercreds_resolve_enabled = False """If True, username/group will be looked up in the OS from peercreds.""" + keep_alive_conn_limit = 10 + """The maximum number of waiting keep-alive connections that will be kept open. + + Default is 10. Set to None to have unlimited connections.""" + def __init__( self, bind_addr, gateway, minthreads=10, maxthreads=-1, server_name=None, @@ -1603,6 +1577,7 @@ self.requests = threadpool.ThreadPool( self, min=minthreads or 1, max=maxthreads, ) + self.connections = connections.ConnectionManager(self) if not server_name: server_name = self.version @@ -1936,7 +1911,7 @@ def prepare_socket(bind_addr, family, type, proto, nodelay, ssl_adapter): """Create and prepare the socket object.""" sock = socket.socket(family, type, proto) - prevent_socket_inheritance(sock) + connections.prevent_socket_inheritance(sock) host, port = bind_addr[:2] IS_EPHEMERAL_PORT = port == 0 @@ -2012,102 +1987,18 @@ def tick(self): """Accept a new connection and put it on the Queue.""" - try: - s, addr = self.socket.accept() - if self.stats['Enabled']: - self.stats['Accepts'] += 1 - if not self.ready: - return - - prevent_socket_inheritance(s) - if hasattr(s, 'settimeout'): - s.settimeout(self.timeout) - - mf = MakeFile - ssl_env = {} - # if ssl cert and key are set, we try to be a secure HTTP server - if self.ssl_adapter is not None: - try: - s, ssl_env = self.ssl_adapter.wrap(s) - except errors.NoSSLError: - msg = ( - 'The client sent a plain HTTP request, but ' - 'this server only speaks HTTPS on this port.' - ) - buf = [ - '%s 400 Bad Request\r\n' % self.protocol, - 'Content-Length: %s\r\n' % len(msg), - 'Content-Type: text/plain\r\n\r\n', - msg, - ] - - sock_to_make = s if not six.PY2 else s._sock - wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE) - try: - wfile.write(''.join(buf).encode('ISO-8859-1')) - except socket.error as ex: - if ex.args[0] not in errors.socket_errors_to_ignore: - raise - return - if not s: - return - mf = self.ssl_adapter.makefile - # Re-apply our timeout since we may have a new socket object - if hasattr(s, 'settimeout'): - s.settimeout(self.timeout) - - conn = self.ConnectionClass(self, s, mf) - - if not isinstance( - self.bind_addr, - (six.text_type, six.binary_type), - ): - # optional values - # Until we do DNS lookups, omit REMOTE_HOST - if addr is None: # sometimes this can happen - # figure out if AF_INET or AF_INET6. - if len(s.getsockname()) == 2: - # AF_INET - addr = ('0.0.0.0', 0) - else: - # AF_INET6 - addr = ('::', 0) - conn.remote_addr = addr[0] - conn.remote_port = addr[1] - - conn.ssl_env = ssl_env + 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() - return - except socket.timeout: - # The only reason for the timeout in start() is so we can - # notice keyboard interrupts on Win32, which don't interrupt - # accept() by default - return - except socket.error as ex: - if self.stats['Enabled']: - self.stats['Socket Errors'] += 1 - if ex.args[0] in errors.socket_error_eintr: - # I *think* this is right. EINTR should occur when a signal - # is received during the accept() call; all docs say retry - # the call, and I *think* I'm reading it right that Python - # will then go ahead and poll for and handle the signal - # elsewhere. See - # https://github.com/cherrypy/cherrypy/issues/707. - return - if ex.args[0] in errors.socket_errors_nonblocking: - # Just try again. See - # https://github.com/cherrypy/cherrypy/issues/479. - return - if ex.args[0] in errors.socket_errors_to_ignore: - # Our socket was closed. - # See https://github.com/cherrypy/cherrypy/issues/686. - return - raise + + self.connections.expire() @property def interrupt(self): @@ -2169,6 +2060,7 @@ sock.close() self.socket = None + self.connections.close() self.requests.stop(self.shutdown_timeout) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot/test/test_conn.py new/cheroot-8.2.1/cheroot/test/test_conn.py --- old/cheroot-7.0.0/cheroot/test/test_conn.py 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/cheroot/test/test_conn.py 2019-10-18 02:59:30.000000000 +0200 @@ -116,6 +116,7 @@ wsgi_server.max_request_body_size = 1001 wsgi_server.timeout = timeout wsgi_server.server_client = wsgi_server_client + wsgi_server.keep_alive_conn_limit = 2 return wsgi_server @@ -389,6 +390,81 @@ test_client.server_instance.protocol = original_server_protocol +def test_keepalive_conn_management(test_client): + """Test management of Keep-Alive connections.""" + test_client.server_instance.timeout = 2 + + def connection(): + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + return http_connection + + def request(conn): + status_line, actual_headers, actual_resp_body = test_client.get( + '/page3', headers=[('Connection', 'Keep-Alive')], + http_conn=conn, protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + + disconnect_errors = ( + http_client.BadStatusLine, + http_client.CannotSendRequest, + http_client.NotConnected, + ) + + # Make a new connection. + c1 = connection() + request(c1) + + # Make a second one. + c2 = connection() + request(c2) + + # Reusing the first connection should still work. + request(c1) + + # Creating a new connection should still work. + c3 = connection() + request(c3) + + # Allow a tick. + time.sleep(0.2) + + # That's three connections, we should expect the one used less recently + # to be expired. + with pytest.raises(disconnect_errors): + request(c2) + + # But the oldest created one should still be valid. + # (As well as the newest one). + request(c1) + request(c3) + + # Wait for some of our timeout. + time.sleep(1.0) + + # Refresh the third connection. + request(c3) + + # Wait for the remainder of our timeout, plus one tick. + time.sleep(1.2) + + # First connection should now be expired. + with pytest.raises(disconnect_errors): + request(c1) + + # But the third one should still be valid. + request(c3) + + test_client.server_instance.timeout = timeout + + @pytest.mark.parametrize( 'timeout_before_headers', ( @@ -539,7 +615,7 @@ response = conn.response_class(conn.sock, method='GET') # there is a bug in python3 regarding the buffering of # ``conn.sock``. Until that bug get's fixed we will - # monkey patch the ``reponse`` instance. + # monkey patch the ``response`` instance. # https://bugs.python.org/issue23377 if not six.PY2: response.fp = conn.sock.makefile('rb', 0) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot/test/test_ssl.py new/cheroot-8.2.1/cheroot/test/test_ssl.py --- old/cheroot-7.0.0/cheroot/test/test_ssl.py 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/cheroot/test/test_ssl.py 2019-10-18 02:59:30.000000000 +0200 @@ -86,7 +86,7 @@ return super(HelloWorldGateway, self).respond() -def make_tls_http_server(bind_addr, ssl_adapter): +def make_tls_http_server(bind_addr, ssl_adapter, request): """Create and start an HTTP server bound to bind_addr.""" httpserver = HTTPServer( bind_addr=bind_addr, @@ -100,28 +100,15 @@ while not httpserver.ready: time.sleep(0.1) + request.addfinalizer(httpserver.stop) + return httpserver @pytest.fixture -def tls_http_server(): +def tls_http_server(request): """Provision a server creator as a fixture.""" - def start_srv(): - bind_addr, ssl_adapter = yield - httpserver = make_tls_http_server(bind_addr, ssl_adapter) - yield httpserver - yield httpserver - - srv_creator = iter(start_srv()) - next(srv_creator) - yield srv_creator - try: - while True: - httpserver = next(srv_creator) - if httpserver is not None: - httpserver.stop() - except StopIteration: - pass + return functools.partial(make_tls_http_server, request=request) @pytest.fixture @@ -183,12 +170,7 @@ tls_certificate.configure_cert(tls_adapter.context) - tlshttpserver = tls_http_server.send( - ( - (interface, port), - tls_adapter, - ), - ) + tlshttpserver = tls_http_server((interface, port), tls_adapter) # testclient = get_server_client(tlshttpserver) # testclient.get('/') @@ -277,12 +259,7 @@ ca.configure_trust(tls_adapter.context) tls_certificate.configure_cert(tls_adapter.context) - tlshttpserver = tls_http_server.send( - ( - (interface, port), - tls_adapter, - ), - ) + tlshttpserver = tls_http_server((interface, port), tls_adapter) interface, _host, port = _get_conn_data(tlshttpserver.bind_addr) @@ -315,6 +292,16 @@ assert resp.text == 'Hello world!' return + # xfail some flaky tests + # https://github.com/cherrypy/cheroot/issues/237 + issue_237 = ( + IS_MACOS + and adapter_type == 'builtin' + and tls_verify_mode != ssl.CERT_NONE + ) + if issue_237: + pytest.xfail('Test sometimes fails') + expected_ssl_errors = ( requests.exceptions.SSLError, OpenSSL.SSL.Error, @@ -414,6 +401,15 @@ tls_certificate_private_key_pem_path, ): """Ensure that connecting over HTTP to HTTPS port is handled.""" + # disable some flaky tests + # https://github.com/cherrypy/cheroot/issues/225 + issue_225 = ( + IS_MACOS + and adapter_type == 'builtin' + ) + if issue_225: + pytest.xfail('Test fails in Travis-CI') + tls_adapter_cls = get_ssl_adapter_class(name=adapter_type) tls_adapter = tls_adapter_cls( tls_certificate_chain_pem_path, tls_certificate_private_key_pem_path, @@ -424,12 +420,7 @@ tls_certificate.configure_cert(tls_adapter.context) interface, _host, port = _get_conn_data(ip_addr) - tlshttpserver = tls_http_server.send( - ( - (interface, port), - tls_adapter, - ), - ) + tlshttpserver = tls_http_server((interface, port), tls_adapter) interface, host, port = _get_conn_data( tlshttpserver.bind_addr, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot/test/webtest.py new/cheroot-8.2.1/cheroot/test/webtest.py --- old/cheroot-7.0.0/cheroot/test/webtest.py 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/cheroot/test/webtest.py 2019-10-18 02:59:30.000000000 +0200 @@ -174,7 +174,7 @@ def getPage( self, url, headers=None, method='GET', body=None, - protocol=None, raise_subcls=None, + protocol=None, raise_subcls=(), ): """Open the url with debugging support. Return status, headers, body. @@ -202,13 +202,17 @@ if isinstance(body, six.text_type): body = body.encode('utf-8') + # for compatibility, support raise_subcls is None + raise_subcls = raise_subcls or () + self.url = url self.time = None start = time.time() result = openURL( url, headers, method, body, self.HOST, self.PORT, self.HTTP_CONN, protocol or self.PROTOCOL, - raise_subcls=raise_subcls, ssl_context=self.ssl_context, + raise_subcls=raise_subcls, + ssl_context=self.ssl_context, ) self.time = time.time() - start self.status, self.headers, self.body = result diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot/workers/threadpool.py new/cheroot-8.2.1/cheroot/workers/threadpool.py --- old/cheroot-7.0.0/cheroot/workers/threadpool.py 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/cheroot/workers/threadpool.py 2019-10-18 02:59:30.000000000 +0200 @@ -8,9 +8,12 @@ import threading import time import socket +import warnings from six.moves import queue +from jaraco.functools import pass_none + __all__ = ('WorkerThread', 'ThreadPool') @@ -108,14 +111,23 @@ if conn is _SHUTDOWNREQUEST: return + # Just close the connection and move on. + if conn.closeable: + conn.close() + continue + self.conn = conn is_stats_enabled = self.server.stats['Enabled'] if is_stats_enabled: self.start_time = time.time() + keep_conn_open = False try: - conn.communicate() + keep_conn_open = conn.communicate() finally: - conn.close() + if keep_conn_open: + self.server.connections.put(conn) + else: + conn.close() if is_stats_enabled: self.requests_seen += self.conn.requests_seen self.bytes_read += self.conn.rfile.bytes_read @@ -243,44 +255,67 @@ Args: timeout (int): time to wait for threads to stop gracefully """ + # for compatability, negative timeouts are treated like None + # TODO: treat negative timeouts like already expired timeouts + if timeout is not None and timeout < 0: + timeout = None + warnings.warning( + 'In the future, negative timeouts to Server.stop() ' + 'will be equivalent to a timeout of zero.', + stacklevel=2, + ) + + if timeout is not None: + endtime = time.time() + timeout + # Must shut down threads here so the code that calls # this method can know when all threads are stopped. for worker in self._threads: self._queue.put(_SHUTDOWNREQUEST) - # Don't join currentThread (when stop is called inside a request). - current = threading.currentThread() - if timeout is not None and timeout >= 0: - endtime = time.time() + timeout - while self._threads: - worker = self._threads.pop() - if worker is not current and worker.is_alive(): - try: - if timeout is None or timeout < 0: - worker.join() - else: - remaining_time = endtime - time.time() - if remaining_time > 0: - worker.join(remaining_time) - if worker.is_alive(): - # We exhausted the timeout. - # Forcibly shut down the socket. - c = worker.conn - if c and not c.rfile.closed: - try: - c.socket.shutdown(socket.SHUT_RD) - except TypeError: - # pyOpenSSL sockets don't take an arg - c.socket.shutdown() - worker.join() - except ( - AssertionError, - # Ignore repeated Ctrl-C. - # See - # https://github.com/cherrypy/cherrypy/issues/691. - KeyboardInterrupt, - ): - pass + ignored_errors = ( + # TODO: explain this exception. + AssertionError, + # Ignore repeated Ctrl-C. See cherrypy#691. + KeyboardInterrupt, + ) + + for worker in self._clear_threads(): + remaining_time = timeout and endtime - time.time() + try: + worker.join(remaining_time) + if worker.is_alive(): + # Timeout exhausted; forcibly shut down the socket. + self._force_close(worker.conn) + worker.join() + except ignored_errors: + pass + + @staticmethod + @pass_none + def _force_close(conn): + if conn.rfile.closed: + return + try: + try: + conn.socket.shutdown(socket.SHUT_RD) + except TypeError: + # pyOpenSSL sockets don't take an arg + conn.socket.shutdown() + except OSError: + # shutdown sometimes fails (race with 'closed' check?) + # ref #238 + pass + + def _clear_threads(self): + """Clear self._threads and yield all joinable threads.""" + # threads = pop_all(self._threads) + threads, self._threads[:] = self._threads[:], [] + return ( + thread + for thread in threads + if thread is not threading.currentThread() + ) @property def qsize(self): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot.egg-info/PKG-INFO new/cheroot-8.2.1/cheroot.egg-info/PKG-INFO --- old/cheroot-7.0.0/cheroot.egg-info/PKG-INFO 2019-09-26 22:59:41.000000000 +0200 +++ new/cheroot-8.2.1/cheroot.egg-info/PKG-INFO 2019-10-18 02:59:50.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: cheroot -Version: 7.0.0 +Version: 8.2.1 Summary: Highly-optimized, pure-python HTTP server Home-page: https://cheroot.cherrypy.org Author: CherryPy Team @@ -102,6 +102,6 @@ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server -Requires-Python: >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* -Provides-Extra: testing +Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7 Provides-Extra: docs +Provides-Extra: testing diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot.egg-info/SOURCES.txt new/cheroot-8.2.1/cheroot.egg-info/SOURCES.txt --- old/cheroot-7.0.0/cheroot.egg-info/SOURCES.txt 2019-09-26 22:59:42.000000000 +0200 +++ new/cheroot-8.2.1/cheroot.egg-info/SOURCES.txt 2019-10-18 02:59:51.000000000 +0200 @@ -5,6 +5,7 @@ .gitignore .pre-commit-config.yaml .pre-commit-config.yaml.failing +.pyup.yml .readthedocs.yml .travis.yml CHANGES.rst @@ -18,6 +19,7 @@ .circleci/config.yml .github/CODE_OF_CONDUCT.md .github/CONTRIBUTING.rst +.github/FUNDING.yml .github/ISSUE_TEMPLATE.md .github/PULL_REQUEST_TEMPLATE.md .github/config.yml @@ -30,6 +32,7 @@ cheroot/__main__.py cheroot/_compat.py cheroot/cli.py +cheroot/connections.py cheroot/errors.py cheroot/makefile.py cheroot/server.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/cheroot.egg-info/requires.txt new/cheroot-8.2.1/cheroot.egg-info/requires.txt --- old/cheroot-7.0.0/cheroot.egg-info/requires.txt 2019-09-26 22:59:41.000000000 +0200 +++ new/cheroot-8.2.1/cheroot.egg-info/requires.txt 2019-10-18 02:59:50.000000000 +0200 @@ -16,7 +16,7 @@ [testing] pytest>=2.8 -pytest-mock==1.10.4 +pytest-mock>=1.11.0 pytest-sugar>=0.9.1 pytest-testmon>=0.9.7 pytest-watch==4.2.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/docs/contribute.rst new/cheroot-8.2.1/docs/contribute.rst --- old/cheroot-7.0.0/docs/contribute.rst 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/docs/contribute.rst 2019-10-18 02:59:30.000000000 +0200 @@ -5,7 +5,7 @@ ~~~~~~~~~~~~~~~~ - You need to install `Python`_ 3 which is required for building docs. -For example, Python 3.7. + For example, Python 3.7. Then, `create and activate a virtual environment`_. And install `tox`_. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/setup.cfg new/cheroot-8.2.1/setup.cfg --- old/cheroot-7.0.0/setup.cfg 2019-09-26 22:59:42.000000000 +0200 +++ new/cheroot-8.2.1/setup.cfg 2019-10-18 02:59:51.000000000 +0200 @@ -76,7 +76,7 @@ collective.checkdocs testing = pytest>=2.8 - pytest-mock==1.10.4 + pytest-mock>=1.11.0 pytest-sugar>=0.9.1 pytest-testmon>=0.9.7 pytest-watch==4.2.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/setup.py new/cheroot-8.2.1/setup.py --- old/cheroot-7.0.0/setup.py 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/setup.py 2019-10-18 02:59:30.000000000 +0200 @@ -1,138 +1,8 @@ -#! /usr/bin/env python +#!/usr/bin/env python + """Cheroot package setuptools installer.""" import setuptools - -try: - from setuptools.config import read_configuration, ConfigOptionsHandler - import setuptools.config - import setuptools.dist - - # Set default value for 'use_scm_version' - setattr(setuptools.dist.Distribution, 'use_scm_version', False) - - # Attach bool parser to 'use_scm_version' option - class ShimConfigOptionsHandler(ConfigOptionsHandler): - """Extension class for ConfigOptionsHandler.""" - - @property - def parsers(self): - """Return an option mapping with default data type parsers.""" - _orig_parsers = super(ShimConfigOptionsHandler, self).parsers - return dict(use_scm_version=self._parse_bool, **_orig_parsers) - - setuptools.config.ConfigOptionsHandler = ShimConfigOptionsHandler -except ImportError: - """This is a shim for setuptools<30.3.""" - import io - import json - - try: - from configparser import ConfigParser, NoSectionError - except ImportError: - from ConfigParser import ConfigParser, NoSectionError - ConfigParser.read_file = ConfigParser.readfp - - def maybe_read_files(d): - """Read files if the string starts with `file:` marker.""" - d = d.strip() - if not d.startswith('file:'): - return d - descs = [] - for fname in map(str.strip, d[5:].split(',')): - with io.open(fname, encoding='utf-8') as f: - descs.append(f.read()) - return ''.join(descs) - - def cfg_val_to_list(v): - """Turn config val to list and filter out empty lines.""" - return list(filter(bool, map(str.strip, v.strip().splitlines()))) - - def cfg_val_to_dict(v): - """Turn config val to dict and filter out empty lines.""" - return dict( - map( - lambda l: list(map(str.strip, l.split('=', 1))), - filter(bool, map(str.strip, v.strip().splitlines())), - ), - ) - - def cfg_val_to_primitive(v): - """Parse primitive config val to appropriate data type.""" - return json.loads(v.strip().lower()) - - def read_configuration(filepath): - """Read metadata and options from setup.cfg located at filepath.""" - cfg = ConfigParser() - with io.open(filepath, encoding='utf-8') as f: - cfg.read_file(f) - - md = dict(cfg.items('metadata')) - for list_key in 'classifiers', 'keywords': - try: - md[list_key] = cfg_val_to_list(md[list_key]) - except KeyError: - pass - try: - md['long_description'] = maybe_read_files(md['long_description']) - except KeyError: - pass - opt = dict(cfg.items('options')) - for list_key in 'use_scm_version', 'zip_safe': - try: - opt[list_key] = cfg_val_to_primitive(opt[list_key]) - except KeyError: - pass - for list_key in 'scripts', 'install_requires', 'setup_requires': - try: - opt[list_key] = cfg_val_to_list(opt[list_key]) - except KeyError: - pass - try: - opt['package_dir'] = cfg_val_to_dict(opt['package_dir']) - except KeyError: - pass - opt_package_data = dict(cfg.items('options.package_data')) - try: - if not opt_package_data.get('', '').strip(): - opt_package_data[''] = opt_package_data['*'] - del opt_package_data['*'] - except KeyError: - pass - try: - opt_extras_require = dict(cfg.items('options.extras_require')) - opt['extras_require'] = {} - for k, v in opt_extras_require.items(): - opt['extras_require'][k] = cfg_val_to_list(v) - except NoSectionError: - pass - opt['package_data'] = {} - for k, v in opt_package_data.items(): - opt['package_data'][k] = cfg_val_to_list(v) - cur_pkgs = opt.get('packages', '').strip() - if '\n' in cur_pkgs: - opt['packages'] = cfg_val_to_list(opt['packages']) - elif cur_pkgs.startswith('find:'): - opt_packages_find = dict(cfg.items('options.packages.find')) - opt['packages'] = setuptools.find_packages(**opt_packages_find) - return {'metadata': md, 'options': opt} - - -setup_params = {} -declarative_setup_params = read_configuration('setup.cfg') - -# Patch incorrectly decoded package_dir option -# ``egg_info`` demands native strings failing with unicode under Python 2 -# Ref https://github.com/pypa/setuptools/issues/1136 -if 'package_dir' in declarative_setup_params['options']: - declarative_setup_params['options']['package_dir'] = { - str(k): str(v) - for k, v in declarative_setup_params['options']['package_dir'].items() - } - -setup_params = dict(setup_params, **declarative_setup_params['metadata']) -setup_params = dict(setup_params, **declarative_setup_params['options']) - - -__name__ == '__main__' and setuptools.setup(**setup_params) +if __name__ == '__main__': + setuptools.setup(use_scm_version=True) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/cheroot-7.0.0/tox.ini new/cheroot-8.2.1/tox.ini --- old/cheroot-7.0.0/tox.ini 2019-09-26 22:59:15.000000000 +0200 +++ new/cheroot-8.2.1/tox.ini 2019-10-18 02:59:30.000000000 +0200 @@ -1,22 +1,12 @@ [tox] envlist = python minversion = 3.13.2 -requires = - pip >= 9 - tox-run-command >= 0.4 [testenv] deps = - pip >= 9 setuptools>=31.0.1 -whitelist_externals = - rm - bash - test commands = - rm -rf .eggs/ - bash -c "if [[ '{env:CIRCLECI:disabled}' == 'disabled' ]]; then pytest --testmon-off {posargs}; fi" - bash -c "if [[ '{env:CIRCLECI:disabled}' != 'disabled' ]]; then circleci tests glob **/test/**.py | circleci tests split --split-by=timings | grep -v '__init__.py' | xargs pytest --testmon-off {posargs}; fi" + pytest --testmon-off {posargs} codecov -f coverage.xml -X gcov usedevelop = True extras = testing @@ -36,6 +26,10 @@ PYTHONDONTWRITEBYTECODE=x WEBTEST_INTERACTIVE=false +[testenv:openssl-version] +commands = + python -m OpenSSL.debug + [testenv:build-docs] basepython = python3.7 description = Build The Docs