diff options
74 files changed, 787 insertions, 1025 deletions
diff --git a/appveyor.yml b/.appveyor.yml index 469d9e9b..e04afe31 100644 --- a/appveyor.yml +++ b/.appveyor.yml @@ -2,8 +2,7 @@ environment: matrix: - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python35-x64" - - PYTHON: "C:\\Python34-x64" - - PYTHON: "C:\\Python27-x64" + - PYTHON: "C:\\Python37-x64" install: # symlink python from a directory with a space @@ -22,7 +21,7 @@ build_script: test_script: - tox -after_test: +on_finish: - ps: | $wc = New-Object 'System.Net.WebClient' $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\.test-results\pytest\results.xml)) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6cee692a..09d34185 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: macos-build: macos: - xcode: "9.2.0" + xcode: "9.3.1" steps: - run: brew install pyenv readline xz @@ -15,20 +15,18 @@ jobs: ' >> $BASH_ENV - run: |- - for py_ver in 2.7.14 3.6.4 3.5.4 3.4.7 pypy3.5-5.10.0 + for py_ver in 3.7.0 3.6.4 3.5.4 pypy3.5-6.0.0 do pyenv install "$py_ver" & done wait - - run: pyenv global 2.7.14 3.6.4 3.5.4 3.4.7 pypy3.5-5.10.0 + - run: pyenv global 3.7.0 3.6.4 3.5.4 pypy3.5-6.0.0 - run: pip install tox tox-pyenv - checkout - - run: tox -e py27,py34,py35,py36,pypy3 -- -p no:sugar + - run: tox -e py35,py36,py37,pypy3 -- -p no:sugar - store_test_results: path: .test-results - - store_test_results: - path: .test-results/pytest - store_artifacts: path: .test-results @@ -39,11 +37,9 @@ jobs: steps: - checkout - run: pip install tox - - run: tox -e py27,py34,py35,py36 + - run: tox -e py35,py36,py37 - store_test_results: path: .test-results - - store_test_results: - path: .test-results/pytest - store_artifacts: path: .test-results diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 68e32d30..ee3e4b86 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -9,32 +9,32 @@ https://stackoverflow.com/questions/tagged/cherrypy **** /DELETE THIS BLOCK **** --> -* **I'm submitting a ...** +**I'm submitting a ...** - [ ] bug report - [ ] feature request - [ ] question about the decisions made in the repository -* **Do you want to request a *feature* or report a *bug*?** +**Do you want to request a *feature* or report a *bug*?** -* **What is the current behavior?** +**What is the current behavior?** -* **If the current behavior is a bug, please provide the steps to reproduce and if possible a screenshots and logs of the problem. If you can, show us your code.** +**If the current behavior is a bug, please provide the steps to reproduce and if possible a screenshots and logs of the problem. If you can, show us your code.** -* **What is the expected behavior?** +**What is the expected behavior?** -* **What is the motivation / use case for changing the behavior?** +**What is the motivation / use case for changing the behavior?** -* **Please tell us about your environment:** +**Please tell us about your environment:** - Cheroot version: X.X.X - CherryPy version: X.X.X @@ -44,4 +44,4 @@ https://stackoverflow.com/questions/tagged/cherrypy -* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, e.g. stackoverflow, gitter, etc.) +**Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, e.g. stackoverflow, gitter, etc.) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 824eb97d..46bbd4f1 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -* **What kind of change does this PR introduce?** +**What kind of change does this PR introduce?** - [ ] bug fix - [ ] feature - [ ] docs update @@ -8,22 +8,22 @@ -* **What is the related issue number (starting with `#`)** +**What is the related issue number (starting with `#`)** -* **What is the current behavior?** (You can also link to an open issue here) +**What is the current behavior?** (You can also link to an open issue here) -* **What is the new behavior (if this is a feature change)?** +**What is the new behavior (if this is a feature change)?** -* **Other information**: +**Other information**: -* **Checklist**: +**Checklist**: - [ ] I think the code is well written - [ ] I wrote [good commit messages][1] diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 00000000..390de12b --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,2 @@ +rtd: + project: cherrypy @@ -33,7 +33,7 @@ sphinx/source/_build .pydevproject # test results in junit format for Appveyor -/.test-results/pytest/results.xml +/.test-results/ # coverage results /.coverage diff --git a/.test-results/pytest/.gitignore b/.test-results/pytest/.gitignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/.test-results/pytest/.gitignore @@ -0,0 +1 @@ +* diff --git a/.travis.yml b/.travis.yml index dbd140b5..66644bad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ language: python os: linux dist: trusty sudo: false +services: +- memcached _base_envs: - &stage_lint stage: &stage_lint_name lint @@ -18,12 +20,12 @@ _base_envs: - _conditions: - &condition_api_or_cron if: type IN (api, cron) +- &no_memcached + services: [] - &pyenv_base <<: *stage_test language: generic - python: &pypy2 pypy2.7-5.10.0 env: - - PYTHON_VERSION=pypy2.7-5.10.0 - &env_pyenv PYENV_ROOT="$HOME/.pyenv" - &env_path PATH="$PYENV_ROOT/bin:$PATH" before_install: @@ -70,18 +72,16 @@ _base_envs: <<: *stage_test_priority - &lint_python_base <<: *stage_lint + <<: *no_memcached python: 3.6 after_failure: skip python: -- 3.4 +- 3.5 - 3.7-dev -- *pypy2 - &pypy3 pypy3.5-5.10.0 jobs: fast_finish: true allow_failures: - # TODO: check what causes testing stuck - - python: *pypy2 # TODO: fix tests - python: *pypy3 - env: TOXENV=pre-commit-pep257 @@ -97,29 +97,21 @@ jobs: - <<: *pure_python_base_priority # mainstream here (3.7) - <<: *pure_python_base_priority - python: 2.7 - - <<: *pure_python_base_priority # mainstream here (3.7) # run tests against the bleeding-edge cheroot env: TOXENV=cheroot-master - <<: *pure_python_base_priority python: nightly - <<: *osx_python_base - python: 2.7 - env: - - PYTHON_VERSION=2.7.13 - - *env_pyenv - - *env_path - - <<: *osx_python_base - python: 3.4 + python: 3.5 env: - - PYTHON_VERSION=3.4.4 + - PYTHON_VERSION=3.5.5 - *env_pyenv - *env_path - <<: *osx_python_base python: *mainstream_python env: - - PYTHON_VERSION=3.6.1 + - PYTHON_VERSION=3.6.5 - *env_pyenv - *env_path - <<: *osx_python_base @@ -139,6 +131,7 @@ jobs: - *env_path - <<: *stage_deploy <<: *python_3_7_mixture + <<: *no_memcached install: skip script: skip deploy: diff --git a/CHANGES.rst b/CHANGES.rst index 56c880fc..6d5ef162 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,37 @@ +v18.1.2 +------- + +* Fixed :issue:`1377` via :pr:`1785`: Restore a native WSGI-less + HTTP server support. +* :pr:`1769`: Reduce log level for non-error events in win32.py + +v18.1.1 +------- + +* :pr:`1774` reverts :pr:`1759` as new evidence emerged that + the original behavior was intentional. Re-opens :issue:`1758`. + +v18.1.0 +------- + +* :issue:`1758` via :pr:`1759`: In the bus, when awaiting a + state change, only publish after the state has changed. + +v18.0.1 +------- + +* :issue:`1738` via :pr:`1736`: Restore support for 'bytes' + in response headers. + +* Substantial removal of Python 2 compatibility code. + +v18.0.0 +------- + +* :issue:`1730`: Drop support for Python 2.7. CherryPy 17 will + remain an LTS release for bug and security fixes. + +* Drop support for Python 3.4. v17.4.2 (unreleased) -------------------- @@ -1,12 +1,23 @@ .. image:: https://img.shields.io/pypi/v/cherrypy.svg :target: https://pypi.org/project/cherrypy +.. image:: https://img.shields.io/badge/Python%203%20only-pip%20install%20%22%3E%3D18.0.0%22-%234da45e.svg + :target: https://python3statement.org/ + +.. image:: https://img.shields.io/badge/Python%203%20and%202-pip%20install%20%22%3C18.0.0%22-%2349a7e9.svg + :target: https://python3statement.org/#sections40-timeline + + + .. image:: https://readthedocs.org/projects/cherrypy/badge/?version=latest :target: https://docs.cherrypy.org/en/latest/?badge=latest .. image:: https://img.shields.io/badge/StackOverflow-CherryPy-blue.svg :target: https://stackoverflow.com/questions/tagged/cheroot+or+cherrypy +.. image:: https://img.shields.io/badge/Mailing%20list-cherrypy--users-orange.svg + :target: https://groups.google.com/group/cherrypy-users + .. image:: https://img.shields.io/gitter/room/cherrypy/cherrypy.svg :target: https://gitter.im/cherrypy/cherrypy diff --git a/cherrypy/_cpchecker.py b/cherrypy/_cpchecker.py index 39b7c972..f26f319c 100644 --- a/cherrypy/_cpchecker.py +++ b/cherrypy/_cpchecker.py @@ -1,9 +1,7 @@ """Checker for CherryPy sites and mounted apps.""" import os import warnings - -import six -from six.moves import builtins +import builtins import cherrypy @@ -70,14 +68,14 @@ class Checker(object): def check_site_config_entries_in_app_config(self): """Check for mounted Applications that have site-scoped config.""" - for sn, app in six.iteritems(cherrypy.tree.apps): + for sn, app in cherrypy.tree.apps.items(): if not isinstance(app, cherrypy.Application): continue msg = [] - for section, entries in six.iteritems(app.config): + for section, entries in app.config.items(): if section.startswith('/'): - for key, value in six.iteritems(entries): + for key, value in entries.items(): for n in ('engine.', 'server.', 'tree.', 'checker.'): if key.startswith(n): msg.append('[%s] %s = %s' % diff --git a/cherrypy/_cpcompat.py b/cherrypy/_cpcompat.py index f454505c..a43f6d36 100644 --- a/cherrypy/_cpcompat.py +++ b/cherrypy/_cpcompat.py @@ -18,145 +18,42 @@ Instead, use unicode literals (from __future__) and bytes literals and their .encode/.decode methods as needed. """ -import re -import sys -import threading - -import six -from six.moves import urllib - - -if six.PY3: - def ntob(n, encoding='ISO-8859-1'): - """Return the given native string as a byte string 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 given native string as a unicode string with the given - encoding. - """ - assert_native(n) - # In Python 3, the native string type is unicode - return n - - def tonative(n, encoding='ISO-8859-1'): - """Return the given string as a native string in the given encoding.""" - # In Python 3, the native string type is unicode - if isinstance(n, bytes): - return n.decode(encoding) - return n -else: - # Python 2 - def ntob(n, encoding='ISO-8859-1'): - """Return the given native string as a byte string 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 given native string as a unicode string 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 six.text_type( # unicode for Python 2 - 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 tonative(n, encoding='ISO-8859-1'): - """Return the given string as a native string in the given encoding.""" - # In Python 2, the native string type is bytes. - if isinstance(n, six.text_type): # unicode for Python 2 - return n.encode(encoding) - return n - - -def assert_native(n): - if not isinstance(n, str): - raise TypeError('n must be a native str (got %s)' % type(n).__name__) - - -# Some platforms don't expose HTTPSConnection, so handle it separately -HTTPSConnection = getattr(six.moves.http_client, 'HTTPSConnection', None) - - -def _unquote_plus_compat(string, encoding='utf-8', errors='replace'): - return urllib.parse.unquote_plus(string).decode(encoding, errors) - - -def _unquote_compat(string, encoding='utf-8', errors='replace'): - return urllib.parse.unquote(string).decode(encoding, errors) +import http.client -def _quote_compat(string, encoding='utf-8', errors='replace'): - return urllib.parse.quote(string.encode(encoding, errors)) - - -unquote_plus = urllib.parse.unquote_plus if six.PY3 else _unquote_plus_compat -unquote = urllib.parse.unquote if six.PY3 else _unquote_compat -quote = urllib.parse.quote if six.PY3 else _quote_compat - -try: - # Prefer simplejson - import simplejson as json -except ImportError: - import json - - -json_decode = json.JSONDecoder().decode -_json_encode = json.JSONEncoder().iterencode - +def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given + encoding. + """ + assert_native(n) + # In Python 3, the native string type is unicode + return n.encode(encoding) -if six.PY3: - # Encode to bytes on Python 3 - def json_encode(value): - for chunk in _json_encode(value): - yield chunk.encode('utf-8') -else: - json_encode = _json_encode +def ntou(n, encoding='ISO-8859-1'): + """Return the given native string as a unicode string with the given + encoding. + """ + assert_native(n) + # In Python 3, the native string type is unicode + return n -text_or_bytes = six.text_type, bytes +def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 3, the native string type is unicode + if isinstance(n, bytes): + return n.decode(encoding) + return n -if sys.version_info >= (3, 3): - Timer = threading.Timer - Event = threading.Event -else: - # Python 3.2 and earlier - Timer = threading._Timer - Event = threading._Event -# html module come in 3.2 version -try: - from html import escape -except ImportError: - from cgi import escape +def assert_native(n): + if not isinstance(n, str): + raise TypeError('n must be a native str (got %s)' % type(n).__name__) -# html module needed the argument quote=False because in cgi the default -# is False. With quote=True the results differ. +# Some platforms don't expose HTTPSConnection, so handle it separately +HTTPSConnection = getattr(http.client, 'HTTPSConnection', None) -def escape_html(s, escape_quote=False): - """Replace special characters "&", "<" and ">" to HTML-safe sequences. - When escape_quote=True, escape (') and (") chars. - """ - return escape(s, quote=escape_quote) +text_or_bytes = str, bytes diff --git a/cherrypy/_cperror.py b/cherrypy/_cperror.py index e2a8fad8..64ca8bb8 100644 --- a/cherrypy/_cperror.py +++ b/cherrypy/_cperror.py @@ -119,17 +119,15 @@ and not simply return an error message as a result. import io import contextlib +import urllib.parse from sys import exc_info as _exc_info from traceback import format_exception as _format_exception from xml.sax import saxutils - -import six -from six.moves import urllib +import html from more_itertools import always_iterable import cherrypy -from cherrypy._cpcompat import escape_html from cherrypy._cpcompat import ntob from cherrypy._cpcompat import tonative from cherrypy._helper import classproperty @@ -274,7 +272,7 @@ class HTTPRedirect(CherryPyException): }[status] msg += '<a href=%s>%s</a>.' msgs = [ - msg % (saxutils.quoteattr(u), escape_html(u)) + msg % (saxutils.quoteattr(u), html.escape(u, quote=False)) for u in self.urls ] response.body = ntob('<br />\n'.join(msgs), 'utf-8') @@ -496,11 +494,11 @@ def get_error_page(status, **kwargs): if kwargs.get('version') is None: kwargs['version'] = cherrypy.__version__ - for k, v in six.iteritems(kwargs): + for k, v in kwargs.items(): if v is None: kwargs[k] = '' else: - kwargs[k] = escape_html(kwargs[k]) + kwargs[k] = html.escape(kwargs[k], quote=False) # Use a custom template or callable for the error page? pages = cherrypy.serving.request.error_page @@ -520,13 +518,13 @@ def get_error_page(status, **kwargs): if cherrypy.lib.is_iterator(result): from cherrypy.lib.encoding import UTF8StreamEncoder return UTF8StreamEncoder(result) - elif isinstance(result, six.text_type): + elif isinstance(result, str): return result.encode('utf-8') else: if not isinstance(result, bytes): raise ValueError( 'error page function did not ' - 'return a bytestring, six.text_type or an ' + 'return a bytestring, str or an ' 'iterator - returned object of type %s.' % (type(result).__name__)) return result diff --git a/cherrypy/_cplogging.py b/cherrypy/_cplogging.py index 53b9addb..151d3b40 100644 --- a/cherrypy/_cplogging.py +++ b/cherrypy/_cplogging.py @@ -113,8 +113,6 @@ import logging import os import sys -import six - import cherrypy from cherrypy import _cperror @@ -155,11 +153,7 @@ class LogManager(object): access_log = None """The actual :class:`logging.Logger` instance for access messages.""" - access_log_format = ( - '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' - if six.PY3 else - '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' - ) + access_log_format = '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' logger_root = None """The "top-level" logger name. @@ -254,8 +248,7 @@ class LogManager(object): status = '-' else: status = response.output_status.split(b' ', 1)[0] - if six.PY3: - status = status.decode('ISO-8859-1') + status = status.decode('ISO-8859-1') atoms = {'h': remote.name or remote.ip, 'l': '-', @@ -270,45 +263,27 @@ class LogManager(object): 'i': request.unique_id, 'z': LazyRfc3339UtcTime(), } - if six.PY3: - for k, v in atoms.items(): - if not isinstance(v, str): - v = str(v) - v = v.replace('"', '\\"').encode('utf8') - # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc - # and backslash for us. All we have to do is strip the quotes. - v = repr(v)[2:-1] - - # in python 3.0 the repr of bytes (as returned by encode) - # uses double \'s. But then the logger escapes them yet, again - # resulting in quadruple slashes. Remove the extra one here. - v = v.replace('\\\\', '\\') - - # Escape double-quote. - atoms[k] = v - - try: - self.access_log.log( - logging.INFO, self.access_log_format.format(**atoms)) - except Exception: - self(traceback=True) - else: - for k, v in atoms.items(): - if isinstance(v, six.text_type): - v = v.encode('utf8') - elif not isinstance(v, str): - v = str(v) - # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc - # and backslash for us. All we have to do is strip the quotes. - v = repr(v)[1:-1] - # Escape double-quote. - atoms[k] = v.replace('"', '\\"') + for k, v in atoms.items(): + if not isinstance(v, str): + v = str(v) + v = v.replace('"', '\\"').encode('utf8') + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[2:-1] + + # in python 3.0 the repr of bytes (as returned by encode) + # uses double \'s. But then the logger escapes them yet, again + # resulting in quadruple slashes. Remove the extra one here. + v = v.replace('\\\\', '\\') + + # Escape double-quote. + atoms[k] = v - try: - self.access_log.log( - logging.INFO, self.access_log_format % atoms) - except Exception: - self(traceback=True) + try: + self.access_log.log( + logging.INFO, self.access_log_format.format(**atoms)) + except Exception: + self(traceback=True) def time(self): """Return now() in Apache Common Log Format (no timezone).""" diff --git a/cherrypy/_cpmodpy.py b/cherrypy/_cpmodpy.py index ac91e625..85db4017 100644 --- a/cherrypy/_cpmodpy.py +++ b/cherrypy/_cpmodpy.py @@ -61,8 +61,6 @@ import os import re import sys -import six - from more_itertools import always_iterable import cherrypy @@ -197,7 +195,7 @@ def handler(req): path = req.uri qs = req.args or '' reqproto = req.protocol - headers = list(six.iteritems(req.headers_in)) + headers = list(req.headers_in.items()) rfile = _ReadOnlyRequest(req) prev = None diff --git a/cherrypy/_cpreqbody.py b/cherrypy/_cpreqbody.py index 893fe5f5..4d3cefe7 100644 --- a/cherrypy/_cpreqbody.py +++ b/cherrypy/_cpreqbody.py @@ -115,30 +115,29 @@ except ImportError: import re import sys import tempfile -try: - from urllib import unquote_plus -except ImportError: - def unquote_plus(bs): - """Bytes version of urllib.parse.unquote_plus.""" - bs = bs.replace(b'+', b' ') - atoms = bs.split(b'%') - for i in range(1, len(atoms)): - item = atoms[i] - try: - pct = int(item[:2], 16) - atoms[i] = bytes([pct]) + item[2:] - except ValueError: - pass - return b''.join(atoms) +from urllib.parse import unquote -import six import cheroot.server import cherrypy -from cherrypy._cpcompat import ntou, unquote +from cherrypy._cpcompat import ntou from cherrypy.lib import httputil +def unquote_plus(bs): + """Bytes version of urllib.parse.unquote_plus.""" + bs = bs.replace(b'+', b' ') + atoms = bs.split(b'%') + for i in range(1, len(atoms)): + item = atoms[i] + try: + pct = int(item[:2], 16) + atoms[i] = bytes([pct]) + item[2:] + except ValueError: + pass + return b''.join(atoms) + + # ------------------------------- Processors -------------------------------- # def process_urlencoded(entity): @@ -986,12 +985,6 @@ class RequestBody(Entity): # add them in here. request_params = self.request_params for key, value in self.params.items(): - # Python 2 only: keyword arguments must be byte strings (type - # 'str'). - if sys.version_info < (3, 0): - if isinstance(key, six.text_type): - key = key.encode('ISO-8859-1') - if key in request_params: if not isinstance(request_params[key], list): request_params[key] = [request_params[key]] diff --git a/cherrypy/_cprequest.py b/cherrypy/_cprequest.py index 3cc0c811..aa42f428 100644 --- a/cherrypy/_cprequest.py +++ b/cherrypy/_cprequest.py @@ -1,11 +1,9 @@ import sys import time +from http.cookies import SimpleCookie, CookieError import uuid -import six -from six.moves.http_cookies import SimpleCookie, CookieError - from more_itertools import consume import cherrypy @@ -141,7 +139,7 @@ def hooks_namespace(k, v): # hookpoint per path (e.g. "hooks.before_handler.1"). # Little-known fact you only get from reading source ;) hookpoint = k.split('.', 1)[0] - if isinstance(v, six.string_types): + if isinstance(v, str): v = cherrypy.lib.reprconf.attributes(v) if not isinstance(v, Hook): v = Hook(v) @@ -704,12 +702,6 @@ class Request(object): 'strings for this resource must be encoded with %r.' % self.query_string_encoding) - # Python 2 only: keyword arguments must be byte strings (type 'str'). - if six.PY2: - for key, value in p.items(): - if isinstance(key, six.text_type): - del p[key] - p[key.encode(self.query_string_encoding)] = value self.params.update(p) def process_headers(self): @@ -786,11 +778,11 @@ class ResponseBody(object): def __set__(self, obj, value): # Convert the given value to an iterable object. - if isinstance(value, six.text_type): + if isinstance(value, str): raise ValueError(self.unicode_err) elif isinstance(value, list): # every item in a list must be bytes... - if any(isinstance(item, six.text_type) for item in value): + if any(isinstance(item, str) for item in value): raise ValueError(self.unicode_err) obj._body = encoding.prepare_iter(value) @@ -903,9 +895,9 @@ class Response(object): if cookie: for line in cookie.split('\r\n'): name, value = line.split(': ', 1) - if isinstance(name, six.text_type): + if isinstance(name, str): name = name.encode('ISO-8859-1') - if isinstance(value, six.text_type): + if isinstance(value, str): value = headers.encode(value) h.append((name, value)) diff --git a/cherrypy/_cpserver.py b/cherrypy/_cpserver.py index 0f60e2c8..5f8d98fa 100644 --- a/cherrypy/_cpserver.py +++ b/cherrypy/_cpserver.py @@ -1,7 +1,5 @@ """Manage HTTP servers with CherryPy.""" -import six - import cherrypy from cherrypy.lib.reprconf import attributes from cherrypy._cpcompat import text_or_bytes @@ -116,21 +114,12 @@ class Server(ServerAdapter): ssl_ciphers = None """The ciphers list of SSL.""" - if six.PY3: - ssl_module = 'builtin' - """The name of a registered SSL adaptation module to use with - the builtin WSGI server. Builtin options are: 'builtin' (to - use the SSL library built into recent versions of Python). - You may also register your own classes in the - cheroot.server.ssl_adapters dict.""" - else: - ssl_module = 'pyopenssl' - """The name of a registered SSL adaptation module to use with the - builtin WSGI server. Builtin options are 'builtin' (to use the SSL - library built into recent versions of Python) and 'pyopenssl' (to - use the PyOpenSSL project, which you must install separately). You - may also register your own classes in the cheroot.server.ssl_adapters - dict.""" + ssl_module = 'builtin' + """The name of a registered SSL adaptation module to use with + the builtin WSGI server. Builtin options are: 'builtin' (to + use the SSL library built into recent versions of Python). + You may also register your own classes in the + cheroot.server.ssl_adapters dict.""" statistics = False """Turns statistics-gathering on or off for aware HTTP servers.""" diff --git a/cherrypy/_cptools.py b/cherrypy/_cptools.py index 57460285..716f99a4 100644 --- a/cherrypy/_cptools.py +++ b/cherrypy/_cptools.py @@ -22,8 +22,6 @@ Tools may be implemented as any object with a namespace. The builtins are generally either modules or instances of the tools.Tool class. """ -import six - import cherrypy from cherrypy._helper import expose @@ -37,14 +35,9 @@ def _getargs(func): """Return the names of all static arguments to the given function.""" # Use this instead of importing inspect for less mem overhead. import types - if six.PY3: - if isinstance(func, types.MethodType): - func = func.__func__ - co = func.__code__ - else: - if isinstance(func, types.MethodType): - func = func.im_func - co = func.func_code + if isinstance(func, types.MethodType): + func = func.__func__ + co = func.__code__ return co.co_varnames[:co.co_argcount] diff --git a/cherrypy/_cptree.py b/cherrypy/_cptree.py index ceb54379..917c5b1a 100644 --- a/cherrypy/_cptree.py +++ b/cherrypy/_cptree.py @@ -2,10 +2,7 @@ import os -import six - import cherrypy -from cherrypy._cpcompat import ntou from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools from cherrypy.lib import httputil, reprconf @@ -289,8 +286,6 @@ class Tree(object): # to '' (some WSGI servers always set SCRIPT_NAME to ''). # Try to look up the app using the full path. env1x = environ - if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0): - env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), env1x.get('PATH_INFO', '')) sn = self.script_name(path or '/') @@ -302,12 +297,6 @@ class Tree(object): # Correct the SCRIPT_NAME and PATH_INFO environ entries. environ = environ.copy() - if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0): - # Python 2/WSGI u.0: all strings MUST be of type unicode - enc = environ[ntou('wsgi.url_encoding')] - environ[ntou('SCRIPT_NAME')] = sn.decode(enc) - environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc) - else: - environ['SCRIPT_NAME'] = sn - environ['PATH_INFO'] = path[len(sn.rstrip('/')):] + environ['SCRIPT_NAME'] = sn + environ['PATH_INFO'] = path[len(sn.rstrip('/')):] return app(environ, start_response) diff --git a/cherrypy/_cpwsgi.py b/cherrypy/_cpwsgi.py index 0b4942ff..c2d36f67 100644 --- a/cherrypy/_cpwsgi.py +++ b/cherrypy/_cpwsgi.py @@ -10,8 +10,6 @@ still be translatable to bytes via the Latin-1 encoding!" import sys as _sys import io -import six - import cherrypy as _cherrypy from cherrypy._cpcompat import ntou from cherrypy import _cperror @@ -28,7 +26,7 @@ def downgrade_wsgi_ux_to_1x(environ): for k, v in list(environ.items()): if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: v = v.encode(url_encoding) - elif isinstance(v, six.text_type): + elif isinstance(v, str): v = v.encode('ISO-8859-1') env1x[k.encode('ISO-8859-1')] = v @@ -177,10 +175,6 @@ class _TrappedResponse(object): def __next__(self): return self.trap(next, self.iter_response) - # todo: https://pythonhosted.org/six/#six.Iterator - if six.PY2: - next = __next__ - def close(self): if hasattr(self.response, 'close'): self.response.close() @@ -198,7 +192,7 @@ class _TrappedResponse(object): if not _cherrypy.request.show_tracebacks: tb = '' s, h, b = _cperror.bare_error(tb) - if six.PY3: + if True: # What fun. s = s.decode('ISO-8859-1') h = [ @@ -238,9 +232,6 @@ class AppResponse(object): def __init__(self, environ, start_response, cpapp): self.cpapp = cpapp try: - if six.PY2: - if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): - environ = downgrade_wsgi_ux_to_1x(environ) self.environ = environ self.run() @@ -262,7 +253,7 @@ class AppResponse(object): raise TypeError(tmpl % v) outheaders.append((k, v)) - if six.PY3: + if True: # According to PEP 3333, when using Python 3, the response # status and headers must be bytes masquerading as unicode; # that is, they must be of type "str" but are restricted to @@ -285,10 +276,6 @@ class AppResponse(object): def __next__(self): return next(self.iter_response) - # todo: https://pythonhosted.org/six/#six.Iterator - if six.PY2: - next = __next__ - def close(self): """Close and de-reference the current request and response. (Core)""" streaming = _cherrypy.serving.response.stream @@ -356,9 +343,6 @@ class AppResponse(object): } def recode_path_qs(self, path, qs): - if not six.PY3: - return - # This isn't perfect; if the given PATH_INFO is in the # wrong encoding, it may fail to match the appropriate config # section URI. But meh. diff --git a/cherrypy/_helper.py b/cherrypy/_helper.py index 314550cb..328e4edb 100644 --- a/cherrypy/_helper.py +++ b/cherrypy/_helper.py @@ -1,7 +1,6 @@ """Helper functions for CP apps.""" -import six -from six.moves import urllib +import urllib.parse from cherrypy._cpcompat import text_or_bytes @@ -26,9 +25,6 @@ def expose(func=None, alias=None): import sys import types decoratable_types = types.FunctionType, types.MethodType, type, - if six.PY2: - # Old-style classes are type types.ClassType. - decoratable_types += types.ClassType, if isinstance(func, decoratable_types): if alias is None: # @expose diff --git a/cherrypy/_json.py b/cherrypy/_json.py new file mode 100644 index 00000000..0c2a0f0e --- /dev/null +++ b/cherrypy/_json.py @@ -0,0 +1,25 @@ +""" +JSON support. + +Expose preferred json module as json and provide encode/decode +convenience functions. +""" + +try: + # Prefer simplejson + import simplejson as json +except ImportError: + import json + + +__all__ = ['json', 'encode', 'decode'] + + +decode = json.JSONDecoder().decode +_encode = json.JSONEncoder().iterencode + + +def encode(value): + """Encode to bytes.""" + for chunk in _encode(value): + yield chunk.encode('utf-8') diff --git a/cherrypy/lib/auth_digest.py b/cherrypy/lib/auth_digest.py index 9b4f55c8..fbb5df64 100644 --- a/cherrypy/lib/auth_digest.py +++ b/cherrypy/lib/auth_digest.py @@ -23,8 +23,7 @@ of plaintext passwords as the credentials store:: import time import functools from hashlib import md5 - -from six.moves.urllib.request import parse_http_list, parse_keqv_list +from urllib.request import parse_http_list, parse_keqv_list import cherrypy from cherrypy._cpcompat import ntob, tonative diff --git a/cherrypy/lib/caching.py b/cherrypy/lib/caching.py index 1673b3c8..38bd636e 100644 --- a/cherrypy/lib/caching.py +++ b/cherrypy/lib/caching.py @@ -37,11 +37,8 @@ import sys import threading import time -import six - import cherrypy from cherrypy.lib import cptools, httputil -from cherrypy._cpcompat import Event class Cache(object): @@ -82,7 +79,7 @@ class AntiStampedeCache(dict): If timeout is None, no waiting is performed nor sentinels used. """ value = self.get(key) - if isinstance(value, Event): + if isinstance(value, threading.Event): if timeout is None: # Ignore the other thread and recalc it ourselves. if debug: @@ -122,7 +119,7 @@ class AntiStampedeCache(dict): """Set the cached value for the given key.""" existing = self.get(key) dict.__setitem__(self, key, value) - if isinstance(existing, Event): + if isinstance(existing, threading.Event): # Set Event.result so other threads waiting on it have # immediate access without needing to poll the cache again. existing.result = value @@ -199,7 +196,7 @@ class MemoryCache(Cache): now = time.time() # Must make a copy of expirations so it doesn't change size # during iteration - items = list(six.iteritems(self.expirations)) + items = list(self.expirations.items()) for expiration_time, objects in items: if expiration_time <= now: for obj_size, uri, sel_header_values in objects: diff --git a/cherrypy/lib/covercp.py b/cherrypy/lib/covercp.py index 0bafca13..3e219713 100644 --- a/cherrypy/lib/covercp.py +++ b/cherrypy/lib/covercp.py @@ -25,8 +25,7 @@ import sys import cgi import os import os.path - -from six.moves import urllib +import urllib.parse import cherrypy diff --git a/cherrypy/lib/cpstats.py b/cherrypy/lib/cpstats.py index ae9f7475..28510887 100644 --- a/cherrypy/lib/cpstats.py +++ b/cherrypy/lib/cpstats.py @@ -193,10 +193,8 @@ import sys import threading import time -import six - import cherrypy -from cherrypy._cpcompat import json +from cherrypy._json import json # ------------------------------- Statistics -------------------------------- # @@ -613,7 +611,7 @@ table.stats2 th { """Return ([headers], [rows]) for the given collection.""" # E.g., the 'Requests' dict. headers = [] - vals = six.itervalues(v) + vals = v.values() for record in vals: for k3 in record: format = formatting.get(k3, missing) diff --git a/cherrypy/lib/cptools.py b/cherrypy/lib/cptools.py index 1c079634..82f897a0 100644 --- a/cherrypy/lib/cptools.py +++ b/cherrypy/lib/cptools.py @@ -3,9 +3,7 @@ import logging import re from hashlib import md5 - -import six -from six.moves import urllib +import urllib.parse import cherrypy from cherrypy._cpcompat import text_or_bytes @@ -307,7 +305,7 @@ class SessionAuth(object): def login_screen(self, from_page='..', username='', error_msg='', **kwargs): - return (six.text_type("""<html><body> + return (str("""<html><body> Message: %(error_msg)s <form method="post" action="do_login"> Login: <input type="text" name="username" value="%(username)s" size="10" /> diff --git a/cherrypy/lib/encoding.py b/cherrypy/lib/encoding.py index 3d001ca6..54a7a8a8 100644 --- a/cherrypy/lib/encoding.py +++ b/cherrypy/lib/encoding.py @@ -2,8 +2,6 @@ import struct import time import io -import six - import cherrypy from cherrypy._cpcompat import text_or_bytes from cherrypy.lib import file_generator @@ -50,7 +48,7 @@ class UTF8StreamEncoder: def __next__(self): res = next(self._iterator) - if isinstance(res, six.text_type): + if isinstance(res, str): res = res.encode('utf-8') return res @@ -99,7 +97,7 @@ class ResponseEncoder: def encoder(body): for chunk in body: - if isinstance(chunk, six.text_type): + if isinstance(chunk, str): chunk = chunk.encode(encoding, self.errors) yield chunk self.body = encoder(self.body) @@ -112,7 +110,7 @@ class ResponseEncoder: self.attempted_charsets.add(encoding) body = [] for chunk in self.body: - if isinstance(chunk, six.text_type): + if isinstance(chunk, str): try: chunk = chunk.encode(encoding, self.errors) except (LookupError, UnicodeError): diff --git a/cherrypy/lib/httputil.py b/cherrypy/lib/httputil.py index 59bcc746..6a7f221b 100644 --- a/cherrypy/lib/httputil.py +++ b/cherrypy/lib/httputil.py @@ -10,17 +10,15 @@ to a public caning. import functools import email.utils import re +import builtins from binascii import b2a_base64 from cgi import parse_header from email.header import decode_header - -import six -from six.moves import range, builtins, map -from six.moves.BaseHTTPServer import BaseHTTPRequestHandler +from http.server import BaseHTTPRequestHandler +from urllib.parse import unquote_plus import cherrypy from cherrypy._cpcompat import ntob, ntou -from cherrypy._cpcompat import unquote_plus response_codes = BaseHTTPRequestHandler.responses.copy() @@ -143,7 +141,7 @@ class HeaderElement(object): return self.value < other.value def __str__(self): - p = [';%s=%s' % (k, v) for k, v in six.iteritems(self.params)] + p = [';%s=%s' % (k, v) for k, v in self.params.items()] return str('%s%s' % (self.value, ''.join(p))) def __bytes__(self): @@ -209,14 +207,11 @@ class AcceptElement(HeaderElement): Ref: https://github.com/cherrypy/cherrypy/issues/1370 """ - six.raise_from( - cherrypy.HTTPError( - 400, - 'Malformed HTTP header: `{}`'. - format(str(self)), - ), - val_err, - ) + raise cherrypy.HTTPError( + 400, + 'Malformed HTTP header: `{}`'. + format(str(self)), + ) from val_err def __cmp__(self, other): diff = builtins.cmp(self.qvalue, other.qvalue) @@ -283,11 +278,11 @@ def valid_status(status): If status has no reason-phrase is supplied, a default reason- phrase will be provided. - >>> from six.moves import http_client - >>> from six.moves.BaseHTTPServer import BaseHTTPRequestHandler - >>> valid_status(http_client.ACCEPTED) == ( - ... int(http_client.ACCEPTED), - ... ) + BaseHTTPRequestHandler.responses[http_client.ACCEPTED] + >>> import http.client + >>> from http.server import BaseHTTPRequestHandler + >>> valid_status(http.client.ACCEPTED) == ( + ... int(http.client.ACCEPTED), + ... ) + BaseHTTPRequestHandler.responses[http.client.ACCEPTED] True """ @@ -295,7 +290,7 @@ def valid_status(status): status = 200 code, reason = status, None - if isinstance(status, six.string_types): + if isinstance(status, str): code, _, reason = status.partition(' ') reason = reason.strip() or None @@ -518,15 +513,14 @@ class HeaderMap(CaseInsensitiveDict): transmitting on the wire for HTTP. """ for k, v in header_items: - if not isinstance(v, six.string_types) and \ - not isinstance(v, six.binary_type): - v = six.text_type(v) + if not isinstance(v, str) and not isinstance(v, bytes): + v = str(v) yield tuple(map(cls.encode_header_item, (k, v))) @classmethod def encode_header_item(cls, item): - if isinstance(item, six.text_type): + if isinstance(item, str): item = cls.encode(item) # See header_translate_* constants above. diff --git a/cherrypy/lib/jsontools.py b/cherrypy/lib/jsontools.py index 48683097..9ca75a8f 100644 --- a/cherrypy/lib/jsontools.py +++ b/cherrypy/lib/jsontools.py @@ -1,5 +1,6 @@ import cherrypy -from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode +from cherrypy import _json as json +from cherrypy._cpcompat import text_or_bytes, ntou def json_processor(entity): @@ -9,7 +10,7 @@ def json_processor(entity): body = entity.fp.read() with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'): - cherrypy.serving.request.json = json_decode(body.decode('utf-8')) + cherrypy.serving.request.json = json.decode(body.decode('utf-8')) def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], @@ -56,7 +57,7 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], def json_handler(*args, **kwargs): value = cherrypy.serving.request._json_inner_handler(*args, **kwargs) - return json_encode(value) + return json.encode(value) def json_out(content_type='application/json', debug=False, diff --git a/cherrypy/lib/reprconf.py b/cherrypy/lib/reprconf.py index fc758490..3976652e 100644 --- a/cherrypy/lib/reprconf.py +++ b/cherrypy/lib/reprconf.py @@ -18,13 +18,13 @@ by adding a named handler to Config.namespaces. The name can be any string, and the handler must be either a callable or a context manager. """ -from cherrypy._cpcompat import text_or_bytes -from six.moves import configparser -from six.moves import builtins - +import builtins +import configparser import operator import sys +from cherrypy._cpcompat import text_or_bytes + class NamespaceSet(dict): @@ -36,7 +36,7 @@ class NamespaceSet(dict): namespace removed) and the config value. Namespace handlers may be any Python callable; they may also be - Python 2.5-style 'context managers', in which case their __enter__ + context managers, in which case their __enter__ method should return a callable to be used as the handler. See cherrypy.tools (the Toolbox class) for an example. """ @@ -61,10 +61,10 @@ class NamespaceSet(dict): bucket[name] = config[k] # I chose __enter__ and __exit__ so someday this could be - # rewritten using Python 2.5's 'with' statement: - # for ns, handler in six.iteritems(self): + # rewritten using 'with' statement: + # for ns, handler in self.items(): # with handler as callable: - # for k, v in six.iteritems(ns_confs.get(ns, {})): + # for k, v in ns_confs.get(ns, {}).items(): # callable(k, v) for ns, handler in self.items(): exit = getattr(handler, '__exit__', None) @@ -211,122 +211,7 @@ class Parser(configparser.ConfigParser): # public domain "unrepr" implementation, found on the web and then improved. -class _Builder2: - - def build(self, o): - m = getattr(self, 'build_' + o.__class__.__name__, None) - if m is None: - raise TypeError('unrepr does not recognize %s' % - repr(o.__class__.__name__)) - return m(o) - - def astnode(self, s): - """Return a Python2 ast Node compiled from a string.""" - try: - import compiler - except ImportError: - # Fallback to eval when compiler package is not available, - # e.g. IronPython 1.0. - return eval(s) - - p = compiler.parse('__tempvalue__ = ' + s) - return p.getChildren()[1].getChildren()[0].getChildren()[1] - - def build_Subscript(self, o): - expr, flags, subs = o.getChildren() - expr = self.build(expr) - subs = self.build(subs) - return expr[subs] - - def build_CallFunc(self, o): - children = o.getChildren() - # Build callee from first child - callee = self.build(children[0]) - # Build args and kwargs from remaining children - args = [] - kwargs = {} - for child in children[1:]: - class_name = child.__class__.__name__ - # None is ignored - if class_name == 'NoneType': - continue - # Keywords become kwargs - if class_name == 'Keyword': - kwargs.update(self.build(child)) - # Everything else becomes args - else: - args.append(self.build(child)) - - return callee(*args, **kwargs) - - def build_Keyword(self, o): - key, value_obj = o.getChildren() - value = self.build(value_obj) - kw_dict = {key: value} - return kw_dict - - def build_List(self, o): - return map(self.build, o.getChildren()) - - def build_Const(self, o): - return o.value - - def build_Dict(self, o): - d = {} - i = iter(map(self.build, o.getChildren())) - for el in i: - d[el] = i.next() - return d - - def build_Tuple(self, o): - return tuple(self.build_List(o)) - - def build_Name(self, o): - name = o.name - if name == 'None': - return None - if name == 'True': - return True - if name == 'False': - return False - - # See if the Name is a package or module. If it is, import it. - try: - return modules(name) - except ImportError: - pass - - # See if the Name is in builtins. - try: - return getattr(builtins, name) - except AttributeError: - pass - - raise TypeError('unrepr could not resolve the name %s' % repr(name)) - - def build_Add(self, o): - left, right = map(self.build, o.getChildren()) - return left + right - - def build_Mul(self, o): - left, right = map(self.build, o.getChildren()) - return left * right - - def build_Getattr(self, o): - parent = self.build(o.expr) - return getattr(parent, o.attrname) - - def build_NoneType(self, o): - return None - - def build_UnarySub(self, o): - return -self.build(o.getChildren()[0]) - - def build_UnaryAdd(self, o): - return self.build(o.getChildren()[0]) - - -class _Builder3: +class _Builder: def build(self, o): m = getattr(self, 'build_' + o.__class__.__name__, None) @@ -441,7 +326,6 @@ class _Builder3: # See if the Name is in builtins. try: - import builtins return getattr(builtins, name) except AttributeError: pass @@ -482,10 +366,7 @@ def unrepr(s): """Return a Python object compiled from a string.""" if not s: return s - if sys.version_info < (3, 0): - b = _Builder2() - else: - b = _Builder3() + b = _Builder() obj = b.astnode(s) return b.build(obj) diff --git a/cherrypy/lib/sessions.py b/cherrypy/lib/sessions.py index 5b49ee13..322c955e 100644 --- a/cherrypy/lib/sessions.py +++ b/cherrypy/lib/sessions.py @@ -106,10 +106,8 @@ import os import time import threading import binascii - -import six -from six.moves import cPickle as pickle -import contextlib2 +import pickle +import contextlib import zc.lockfile @@ -119,10 +117,6 @@ from cherrypy.lib import locking from cherrypy.lib import is_iterator -if six.PY2: - FileNotFoundError = OSError - - missing = object() @@ -410,7 +404,7 @@ class RamSession(Session): """Clean up expired sessions.""" now = self.now() - for _id, (data, expiration_time) in list(six.iteritems(self.cache)): + for _id, (data, expiration_time) in list(self.cache.items()): if expiration_time <= now: try: del self.cache[_id] @@ -572,7 +566,7 @@ class FileSession(Session): def release_lock(self, path=None): """Release the lock on the currently-loaded session data.""" self.lock.close() - with contextlib2.suppress(FileNotFoundError): + with contextlib.suppress(FileNotFoundError): os.remove(self.lock._path) self.locked = False @@ -624,7 +618,7 @@ class MemcachedSession(Session): # This is a separate set of locks per session id. locks = {} - servers = ['127.0.0.1:11211'] + servers = ['localhost:11211'] @classmethod def setup(cls, **kwargs): diff --git a/cherrypy/lib/static.py b/cherrypy/lib/static.py index da9d9373..9a3b8e83 100644 --- a/cherrypy/lib/static.py +++ b/cherrypy/lib/static.py @@ -5,12 +5,11 @@ import platform import re import stat import mimetypes +import urllib.parse from email.generator import _make_boundary as make_boundary from io import UnsupportedOperation -from six.moves import urllib - import cherrypy from cherrypy._cpcompat import ntob from cherrypy.lib import cptools, httputil, file_generator_limited diff --git a/cherrypy/lib/xmlrpcutil.py b/cherrypy/lib/xmlrpcutil.py index ddaac86a..29d9c4a2 100644 --- a/cherrypy/lib/xmlrpcutil.py +++ b/cherrypy/lib/xmlrpcutil.py @@ -1,7 +1,6 @@ """XML-RPC tool helpers.""" import sys - -from six.moves.xmlrpc_client import ( +from xmlrpc.client import ( loads as xmlrpc_loads, dumps as xmlrpc_dumps, Fault as XMLRPCFault ) diff --git a/cherrypy/process/plugins.py b/cherrypy/process/plugins.py index 8c246c81..d2f87a4d 100644 --- a/cherrypy/process/plugins.py +++ b/cherrypy/process/plugins.py @@ -6,11 +6,10 @@ import signal as _signal import sys import time import threading - -from six.moves import _thread +import _thread from cherrypy._cpcompat import text_or_bytes -from cherrypy._cpcompat import ntob, Timer +from cherrypy._cpcompat import ntob # _module__file__base is used by Autoreload to make # absolute any filenames retrieved from sys.modules which are not @@ -452,7 +451,7 @@ class PIDFile(SimplePlugin): pass -class PerpetualTimer(Timer): +class PerpetualTimer(threading.Timer): """A responsive subclass of threading.Timer whose run() method repeats. @@ -627,7 +626,10 @@ class Autoreloader(Monitor): def sysfiles(self): """Return a Set of sys.modules filenames to monitor.""" - search_mod_names = filter(re.compile(self.match).match, sys.modules) + search_mod_names = filter( + re.compile(self.match).match, + list(sys.modules.keys()), + ) mods = map(sys.modules.get, search_mod_names) return set(filter(None, map(self._file_for_module, mods))) diff --git a/cherrypy/process/win32.py b/cherrypy/process/win32.py index 096b0278..b7a79b1b 100644 --- a/cherrypy/process/win32.py +++ b/cherrypy/process/win32.py @@ -20,7 +20,7 @@ class ConsoleCtrlHandler(plugins.SimplePlugin): def start(self): if self.is_set: - self.bus.log('Handler for console events already set.', level=40) + self.bus.log('Handler for console events already set.', level=20) return result = win32api.SetConsoleCtrlHandler(self.handle, 1) @@ -28,12 +28,12 @@ class ConsoleCtrlHandler(plugins.SimplePlugin): self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % win32api.GetLastError(), level=40) else: - self.bus.log('Set handler for console events.', level=40) + self.bus.log('Set handler for console events.', level=20) self.is_set = True def stop(self): if not self.is_set: - self.bus.log('Handler for console events already off.', level=40) + self.bus.log('Handler for console events already off.', level=20) return try: @@ -46,7 +46,7 @@ class ConsoleCtrlHandler(plugins.SimplePlugin): self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % win32api.GetLastError(), level=40) else: - self.bus.log('Removed handler for console events.', level=40) + self.bus.log('Removed handler for console events.', level=20) self.is_set = False def handle(self, event): diff --git a/cherrypy/process/wspbus.py b/cherrypy/process/wspbus.py index d91dba48..ead90a4e 100644 --- a/cherrypy/process/wspbus.py +++ b/cherrypy/process/wspbus.py @@ -81,7 +81,7 @@ import warnings import subprocess import functools -import six +from more_itertools import always_iterable # Here I save the value of os.getcwd(), which, if I am imported early enough, @@ -370,10 +370,7 @@ class Bus(object): def wait(self, state, interval=0.1, channel=None): """Poll for the given state(s) at intervals; publish to channel.""" - if isinstance(state, (tuple, list)): - states = state - else: - states = [state] + states = set(always_iterable(state)) while self.state not in states: time.sleep(interval) @@ -436,7 +433,7 @@ class Bus(object): :seealso: http://stackoverflow.com/a/28414807/595220 """ try: - char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p + char_p = ctypes.c_wchar_p argv = ctypes.POINTER(char_p)() argc = ctypes.c_int() diff --git a/cherrypy/test/helper.py b/cherrypy/test/helper.py index 01c5a0c0..85c8f7eb 100644 --- a/cherrypy/test/helper.py +++ b/cherrypy/test/helper.py @@ -10,10 +10,10 @@ import sys import time import unittest import warnings +import contextlib import portend import pytest -import six from cheroot.test import webtest @@ -93,7 +93,7 @@ class LocalSupervisor(Supervisor): cherrypy.engine.exit() - servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {}))) + servers_copy = list(getattr(cherrypy, 'servers', {}).items()) for name, server in servers_copy: server.unsubscribe() del cherrypy.servers[name] @@ -449,7 +449,7 @@ server.ssl_private_key: r'%s' 'extra': extra, } with io.open(self.config_file, 'w', encoding='utf-8') as f: - f.write(six.text_type(conf)) + f.write(str(conf)) def start(self, imports=None): """Start cherryd in a subprocess.""" @@ -523,20 +523,5 @@ server.ssl_private_key: r'%s' self._proc.wait() def _join_daemon(self): - try: - try: - # Mac, UNIX - os.wait() - except AttributeError: - # Windows - try: - pid = self.get_pid() - except IOError: - # Assume the subprocess deleted the pidfile on shutdown. - pass - else: - os.waitpid(pid, 0) - except OSError: - x = sys.exc_info()[1] - if x.args != (10, 'No child processes'): - raise + with contextlib.suppress(IOError): + os.waitpid(self.get_pid(), 0) diff --git a/cherrypy/test/logtest.py b/cherrypy/test/logtest.py index ed8f1540..c6edc5d0 100644 --- a/cherrypy/test/logtest.py +++ b/cherrypy/test/logtest.py @@ -4,9 +4,7 @@ import sys import time from uuid import UUID -import six - -from cherrypy._cpcompat import text_or_bytes, ntob +from cherrypy._cpcompat import text_or_bytes try: @@ -105,7 +103,9 @@ class LogCase(object): self.lastmarker = key open(self.logfile, 'ab+').write( - ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8')) + b'%s%s\n' + % (self.markerPrefix, key.encode('utf-8')) + ) def _read_marked_region(self, marker=None): """Return lines from self.logfile in the marked region. @@ -121,7 +121,7 @@ class LogCase(object): if marker is None: return open(logfile, 'rb').readlines() - if isinstance(marker, six.text_type): + if isinstance(marker, str): marker = marker.encode('utf-8') data = [] in_region = False @@ -201,7 +201,7 @@ class LogCase(object): # Single arg. Use __getitem__ and allow lines to be str or list. if isinstance(lines, (tuple, list)): lines = lines[0] - if isinstance(lines, six.text_type): + if isinstance(lines, str): lines = lines.encode('utf-8') if lines not in data[sliceargs]: msg = '%r not found on log line %r' % (lines, sliceargs) @@ -221,7 +221,7 @@ class LogCase(object): start, stop = sliceargs for line, logline in zip(lines, data[start:stop]): - if isinstance(line, six.text_type): + if isinstance(line, str): line = line.encode('utf-8') if line not in logline: msg = '%r not found in log' % line diff --git a/cherrypy/test/sessiondemo.py b/cherrypy/test/sessiondemo.py index 8226c1b9..3849a259 100755 --- a/cherrypy/test/sessiondemo.py +++ b/cherrypy/test/sessiondemo.py @@ -5,8 +5,6 @@ import calendar from datetime import datetime import sys -import six - import cherrypy from cherrypy.lib import sessions @@ -123,7 +121,7 @@ class Root(object): 'changemsg': '<br>'.join(changemsg), 'respcookie': cherrypy.response.cookie.output(), 'reqcookie': cherrypy.request.cookie.output(), - 'sessiondata': list(six.iteritems(cherrypy.session)), + 'sessiondata': list(cherrypy.session.items()), 'servertime': ( datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC' ), diff --git a/cherrypy/test/test_auth_digest.py b/cherrypy/test/test_auth_digest.py index 512e39a5..745f89e6 100644 --- a/cherrypy/test/test_auth_digest.py +++ b/cherrypy/test/test_auth_digest.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 -import six - import cherrypy from cherrypy.lib import auth_digest @@ -92,8 +90,7 @@ class DigestAuthTest(helper.CPWebCase): 'cnonce="1522e61005789929"') encoded_user = username - if six.PY3: - encoded_user = encoded_user.encode('utf-8') + encoded_user = encoded_user.encode('utf-8') encoded_user = encoded_user.decode('latin1') auth_header = base_auth % ( encoded_user, realm, nonce, test_uri, diff --git a/cherrypy/test/test_bus.py b/cherrypy/test/test_bus.py index 6026b47e..12a516c9 100644 --- a/cherrypy/test/test_bus.py +++ b/cherrypy/test/test_bus.py @@ -1,6 +1,6 @@ import threading import time -import unittest +import unittest.mock from cherrypy.process import wspbus @@ -179,12 +179,14 @@ class BusMethodTests(unittest.TestCase): time.sleep(0.2) getattr(b, method)() - for method, states in [('start', [b.states.STARTED]), - ('stop', [b.states.STOPPED]), - ('start', - [b.states.STARTING, b.states.STARTED]), - ('exit', [b.states.EXITING]), - ]: + flow = [ + ('start', [b.states.STARTED]), + ('stop', [b.states.STOPPED]), + ('start', [b.states.STARTING, b.states.STARTED]), + ('exit', [b.states.EXITING]), + ] + + for method, states in flow: threading.Thread(target=f, args=(method,)).start() b.wait(states) @@ -192,6 +194,18 @@ class BusMethodTests(unittest.TestCase): if b.state not in states: self.fail('State %r not in %r' % (b.state, states)) + def test_wait_publishes_periodically(self): + bus = wspbus.Bus() + callback = unittest.mock.MagicMock() + bus.subscribe('main', callback) + + def set_start(): + time.sleep(0.05) + bus.start() + threading.Thread(target=set_start).start() + bus.wait(bus.states.STARTED, interval=0.01, channel='main') + assert callback.call_count > 3 + def test_block(self): b = wspbus.Bus() self.log(b) diff --git a/cherrypy/test/test_caching.py b/cherrypy/test/test_caching.py index 1a6ed4f2..19ef05bd 100644 --- a/cherrypy/test/test_caching.py +++ b/cherrypy/test/test_caching.py @@ -3,9 +3,7 @@ from itertools import count import os import threading import time - -from six.moves import range -from six.moves import urllib +import urllib.parse import pytest diff --git a/cherrypy/test/test_compat.py b/cherrypy/test/test_compat.py deleted file mode 100644 index 44a9fa31..00000000 --- a/cherrypy/test/test_compat.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Test Python 2/3 compatibility module.""" -from __future__ import unicode_literals - -import unittest - -import pytest -import six - -from cherrypy import _cpcompat as compat - - -class StringTester(unittest.TestCase): - """Tests for string conversion.""" - - @pytest.mark.skipif(six.PY3, reason='Only useful on Python 2') - def test_ntob_non_native(self): - """ntob should raise an Exception on unicode. - - (Python 2 only) - - See #1132 for discussion. - """ - self.assertRaises(TypeError, compat.ntob, 'fight') - - -class EscapeTester(unittest.TestCase): - """Class to test escape_html function from _cpcompat.""" - - def test_escape_quote(self): - """test_escape_quote - Verify the output for &<>"' chars.""" - self.assertEqual( - """xx&<>"aa'""", - compat.escape_html("""xx&<>"aa'"""), - ) diff --git a/cherrypy/test/test_config.py b/cherrypy/test/test_config.py index be17df90..4dfea74d 100644 --- a/cherrypy/test/test_config.py +++ b/cherrypy/test/test_config.py @@ -5,8 +5,6 @@ import os import sys import unittest -import six - import cherrypy from cherrypy.test import helper @@ -16,7 +14,7 @@ localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) def StringIOFromNative(x): - return io.StringIO(six.text_type(x)) + return io.StringIO(str(x)) def setup_server(): @@ -105,18 +103,12 @@ def setup_server(): def incr(self, num): return num + 1 - if not six.PY3: - thing3 = "thing3: unicode('test', errors='ignore')" - else: - thing3 = '' - ioconf = StringIOFromNative(""" [/] neg: -1234 filename: os.path.join(sys.prefix, "hello.py") thing1: cherrypy.lib.httputil.response_codes[404] thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 -%s complex: 3+2j mul: 6*3 ones: "11" @@ -125,7 +117,7 @@ stradd: %%(ones)s + %%(twos)s + "33" [/favicon.ico] tools.staticfile.filename = %r -""" % (thing3, os.path.join(localDir, 'static/dirback.jpg'))) +""" % os.path.join(localDir, 'static/dirback.jpg')) root = Root() root.foo = Foo() @@ -203,10 +195,6 @@ class ConfigTests(helper.CPWebCase): from cherrypy.tutorial import thing2 self.assertBody(repr(thing2)) - if not six.PY3: - self.getPage('/repr?key=thing3') - self.assertBody(repr(six.text_type('test'))) - self.getPage('/repr?key=complex') self.assertBody('(3+2j)') diff --git a/cherrypy/test/test_conn.py b/cherrypy/test/test_conn.py index 7d60c6fb..1e160b4e 100644 --- a/cherrypy/test/test_conn.py +++ b/cherrypy/test/test_conn.py @@ -4,10 +4,8 @@ import errno import socket import sys import time - -import six -from six.moves import urllib -from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected +import urllib.parse +from http.client import BadStatusLine, HTTPConnection, NotConnected import pytest @@ -91,7 +89,7 @@ def setup_server(): body = [body] newbody = [] for chunk in body: - if isinstance(chunk, six.text_type): + if isinstance(chunk, str): chunk = chunk.encode('ISO-8859-1') newbody.append(chunk) return newbody @@ -441,8 +439,7 @@ class PipelineTests(helper.CPWebCase): # ``conn.sock``. Until that bug get's fixed we will # monkey patch the ``response`` instance. # https://bugs.python.org/issue23377 - if six.PY3: - response.fp = conn.sock.makefile('rb', 0) + response.fp = conn.sock.makefile('rb', 0) response.begin() body = response.read(13) self.assertEqual(response.status, 200) diff --git a/cherrypy/test/test_core.py b/cherrypy/test/test_core.py index 9834c1f3..eae90b10 100644 --- a/cherrypy/test/test_core.py +++ b/cherrypy/test/test_core.py @@ -6,8 +6,6 @@ import os import sys import types -import six - import cherrypy from cherrypy._cpcompat import ntou from cherrypy import _cptools, tools @@ -57,7 +55,7 @@ class CoreRequestHandlingTest(helper.CPWebCase): """ def __init__(cls, name, bases, dct): type.__init__(cls, name, bases, dct) - for value in six.itervalues(dct): + for value in dct.values(): if isinstance(value, types.FunctionType): value.exposed = True setattr(root, name.lower(), cls()) diff --git a/cherrypy/test/test_dynamicobjectmapping.py b/cherrypy/test/test_dynamicobjectmapping.py index 725a3ce0..aaa89ca7 100644 --- a/cherrypy/test/test_dynamicobjectmapping.py +++ b/cherrypy/test/test_dynamicobjectmapping.py @@ -1,5 +1,3 @@ -import six - import cherrypy from cherrypy.test import helper @@ -79,7 +77,7 @@ def setup_server(): self.name = name def __unicode__(self): - return six.text_type(self.name) + return str(self.name) def __str__(self): return str(self.name) @@ -105,7 +103,7 @@ def setup_server(): return 'POST %d' % make_user(name) def GET(self): - return six.text_type(sorted(user_lookup.keys())) + return str(sorted(user_lookup.keys())) def dynamic_dispatch(self, vpath): try: @@ -130,7 +128,7 @@ def setup_server(): """ Return the appropriate representation of the instance. """ - return six.text_type(self.user) + return str(self.user) def POST(self, name): """ diff --git a/cherrypy/test/test_encoding.py b/cherrypy/test/test_encoding.py index 26b0aa18..882d7a5b 100644 --- a/cherrypy/test/test_encoding.py +++ b/cherrypy/test/test_encoding.py @@ -3,9 +3,8 @@ import gzip import io from unittest import mock - -from six.moves.http_client import IncompleteRead -from six.moves.urllib.parse import quote as url_quote +from http.client import IncompleteRead +from urllib.parse import quote as url_quote import cherrypy from cherrypy._cpcompat import ntob, ntou diff --git a/cherrypy/test/test_http.py b/cherrypy/test/test_http.py index 0899d4d0..a808cc3a 100644 --- a/cherrypy/test/test_http.py +++ b/cherrypy/test/test_http.py @@ -6,13 +6,11 @@ import mimetypes import socket import sys from unittest import mock - -import six -from six.moves.http_client import HTTPConnection -from six.moves import urllib +import urllib.parse +from http.client import HTTPConnection import cherrypy -from cherrypy._cpcompat import HTTPSConnection, quote +from cherrypy._cpcompat import HTTPSConnection from cherrypy.test import helper @@ -36,7 +34,7 @@ def encode_filename(filename): """ if is_ascii(filename): return 'filename', '"{filename}"'.format(**locals()) - encoded = quote(filename, encoding='utf-8') + encoded = urllib.parse.quote(filename, encoding='utf-8') return 'filename*', "'".join(( 'UTF-8', '', # lang @@ -105,14 +103,12 @@ class HTTPTests(helper.CPWebCase): count += 1 else: if count: - if six.PY3: - curchar = chr(curchar) + curchar = chr(curchar) summary.append('%s * %d' % (curchar, count)) count = 1 curchar = c if count: - if six.PY3: - curchar = chr(curchar) + curchar = chr(curchar) summary.append('%s * %d' % (curchar, count)) return ', '.join(summary) diff --git a/cherrypy/test/test_httputil.py b/cherrypy/test/test_httputil.py index 656b8a3d..fe6a3f41 100644 --- a/cherrypy/test/test_httputil.py +++ b/cherrypy/test/test_httputil.py @@ -1,6 +1,6 @@ """Test helpers from ``cherrypy.lib.httputil`` module.""" import pytest -from six.moves import http_client +import http.client from cherrypy.lib import httputil @@ -49,12 +49,12 @@ EXPECTED_444 = (444, 'Non-existent reason', '') (None, EXPECTED_200), (200, EXPECTED_200), ('500', EXPECTED_500), - (http_client.NOT_FOUND, EXPECTED_404), + (http.client.NOT_FOUND, EXPECTED_404), ('444 Non-existent reason', EXPECTED_444), ] ) def test_valid_status(status, expected_status): - """Check valid int, string and http_client-constants + """Check valid int, string and http.client-constants statuses processing.""" assert httputil.valid_status(status) == expected_status diff --git a/cherrypy/test/test_iterator.py b/cherrypy/test/test_iterator.py index 92f08e7c..6600a78d 100644 --- a/cherrypy/test/test_iterator.py +++ b/cherrypy/test/test_iterator.py @@ -1,5 +1,3 @@ -import six - import cherrypy from cherrypy.test import helper @@ -88,7 +86,7 @@ class IteratorTest(helper.CPWebCase): @cherrypy.expose def count(self, clsname): cherrypy.response.headers['Content-Type'] = 'text/plain' - return six.text_type(globals()[clsname].created) + return str(globals()[clsname].created) @cherrypy.expose def getall(self, clsname): @@ -139,7 +137,7 @@ class IteratorTest(helper.CPWebCase): headers = response.getheaders() for header_name, header_value in headers: if header_name.lower() == 'content-length': - expected = six.text_type(1024 * 16 * 256) + expected = str(1024 * 16 * 256) assert header_value == expected, header_value break else: diff --git a/cherrypy/test/test_json.py b/cherrypy/test/test_json.py index 1585f6e6..4b8be548 100644 --- a/cherrypy/test/test_json.py +++ b/cherrypy/test/test_json.py @@ -1,7 +1,6 @@ import cherrypy from cherrypy.test import helper - -from cherrypy._cpcompat import json +from cherrypy._json import json json_out = cherrypy.config(**{'tools.json_out.on': True}) diff --git a/cherrypy/test/test_logging.py b/cherrypy/test/test_logging.py index c4948c20..5abebbf1 100644 --- a/cherrypy/test/test_logging.py +++ b/cherrypy/test/test_logging.py @@ -1,9 +1,11 @@ """Basic tests for the CherryPy core: request handling.""" +import logging import os from unittest import mock -import six +import pytest +import requests # FIXME: Temporary using it directly, better switch import cherrypy from cherrypy._cpcompat import ntou @@ -18,6 +20,25 @@ tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape') erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape') +@pytest.fixture +def server(): + setup_server() + cherrypy.engine.start() + + yield + + shutdown_server() + + +def shutdown_server(): + cherrypy.engine.exit() + + servers_copy = list(getattr(cherrypy, 'servers', {}).items()) + for name, server in servers_copy: + server.unsubscribe() + del cherrypy.servers[name] + + def setup_server(): class Root: @@ -109,9 +130,7 @@ class AccessLogTests(helper.CPWebCase, logtest.LogCase): @mock.patch( 'cherrypy._cplogging.LogManager.access_log_format', - '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}' - if six.PY3 else - '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s' + '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}', ) def testCustomLogFormat(self): """Test a customized access_log_format string, which is a @@ -126,9 +145,7 @@ class AccessLogTests(helper.CPWebCase, logtest.LogCase): @mock.patch( 'cherrypy._cplogging.LogManager.access_log_format', - '{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}' - if six.PY3 else - '%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s' + '{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}', ) def testTimezLogFormat(self): """Test a customized access_log_format string, which is a @@ -150,7 +167,7 @@ class AccessLogTests(helper.CPWebCase, logtest.LogCase): @mock.patch( 'cherrypy._cplogging.LogManager.access_log_format', - '{i}' if six.PY3 else '%(i)s' + '{i}', ) def testUUIDv4ParameterLogFormat(self): """Test rendering of UUID4 within access log.""" @@ -163,11 +180,8 @@ class AccessLogTests(helper.CPWebCase, logtest.LogCase): self.markLog() self.getPage('/uni_code') self.assertStatus(200) - if six.PY3: - # The repr of a bytestring in six.PY3 includes a b'' prefix - self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1]) - else: - self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1]) + # The repr of a bytestring includes a b'' prefix + self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1]) # Test the erebos value. Included inline for your enlightenment. # Note the 'r' prefix--those backslashes are literals. self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82') @@ -176,10 +190,7 @@ class AccessLogTests(helper.CPWebCase, logtest.LogCase): self.markLog() self.getPage('/slashes') self.assertStatus(200) - if six.PY3: - self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"') - else: - self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"') + self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"') # Test whitespace in output. self.markLog() @@ -189,21 +200,14 @@ class AccessLogTests(helper.CPWebCase, logtest.LogCase): self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"') -class ErrorLogTests(helper.CPWebCase, logtest.LogCase): - setup_server = staticmethod(setup_server) +@pytest.mark.xfail(reason='failing') +def test_tracebacks(server, caplog): + with caplog.at_level(logging.ERROR, logger='cherrypy.error'): + resp = requests.get('http://127.0.0.1:8080/error') - logfile = error_log + rec = caplog.records[0] + exc_cls, exc_msg = rec.exc_info[0], rec.message - def testTracebacks(self): - # Test that tracebacks get written to the error log. - self.markLog() - ignore = helper.webtest.ignored_exceptions - ignore.append(ValueError) - try: - self.getPage('/error') - self.assertInBody('raise ValueError()') - self.assertLog(0, 'HTTP') - self.assertLog(1, 'Traceback (most recent call last):') - self.assertLog(-2, 'raise ValueError()') - finally: - ignore.pop() + assert 'raise ValueError()' in resp.text + assert 'HTTP' in exc_msg + assert exc_cls is ValueError diff --git a/cherrypy/test/test_refleaks.py b/cherrypy/test/test_refleaks.py index c2fe9e66..95813679 100644 --- a/cherrypy/test/test_refleaks.py +++ b/cherrypy/test/test_refleaks.py @@ -3,8 +3,7 @@ import itertools import platform import threading - -from six.moves.http_client import HTTPConnection +from http.client import HTTPConnection import cherrypy from cherrypy._cpcompat import HTTPSConnection diff --git a/cherrypy/test/test_request_obj.py b/cherrypy/test/test_request_obj.py index 6b93e13d..31023e8f 100644 --- a/cherrypy/test/test_request_obj.py +++ b/cherrypy/test/test_request_obj.py @@ -5,9 +5,7 @@ import os import sys import types import uuid - -import six -from six.moves.http_client import IncompleteRead +from http.client import IncompleteRead import cherrypy from cherrypy._cpcompat import ntou @@ -243,7 +241,7 @@ class RequestObjectTests(helper.CPWebCase): def ifmatch(self): val = cherrypy.request.headers['If-Match'] - assert isinstance(val, six.text_type) + assert isinstance(val, str) cherrypy.response.headers['ETag'] = val return val @@ -251,7 +249,7 @@ class RequestObjectTests(helper.CPWebCase): def get_elements(self, headername): e = cherrypy.request.headers.elements(headername) - return '\n'.join([six.text_type(x) for x in e]) + return '\n'.join([str(x) for x in e]) class Method(Test): diff --git a/cherrypy/test/test_session.py b/cherrypy/test/test_session.py index 0083c97c..0d82a304 100755 --- a/cherrypy/test/test_session.py +++ b/cherrypy/test/test_session.py @@ -1,25 +1,24 @@ import os +import platform import threading import time -import socket -import importlib - -from six.moves.http_client import HTTPConnection +from http.client import HTTPConnection +from distutils.spawn import find_executable import pytest from path import Path +from more_itertools import consume +import portend import cherrypy -from cherrypy._cpcompat import ( - json_decode, - HTTPSConnection, -) +from cherrypy._cpcompat import HTTPSConnection from cherrypy.lib import sessions from cherrypy.lib import reprconf from cherrypy.lib.httputil import response_codes from cherrypy.test import helper +from cherrypy import _json as json -localDir = os.path.dirname(__file__) +localDir = Path(__file__).dirname() def http_methods_allowed(methods=['GET', 'HEAD']): @@ -48,9 +47,10 @@ def setup_server(): cherrypy.session.cache.clear() @cherrypy.expose + @cherrypy.tools.json_out() def data(self): cherrypy.session['aha'] = 'foo' - return repr(cherrypy.session._data) + return cherrypy.session._data @cherrypy.expose def testGen(self): @@ -142,14 +142,18 @@ def setup_server(): class SessionTest(helper.CPWebCase): setup_server = staticmethod(setup_server) - def tearDown(self): - # Clean up sessions. - for fname in os.listdir(localDir): - if fname.startswith(sessions.FileSession.SESSION_PREFIX): - path = Path(localDir) / fname - path.remove_p() + @classmethod + def teardown_class(cls): + """Clean up sessions.""" + super(cls, cls).teardown_class() + consume( + file.remove_p() + for file in localDir.listdir() + if file.basename().startswith( + sessions.FileSession.SESSION_PREFIX + ) + ) - @pytest.mark.xfail(reason='#1534') def test_0_Session(self): self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession') self.getPage('/clear') @@ -157,62 +161,62 @@ class SessionTest(helper.CPWebCase): # Test that a normal request gets the same id in the cookies. # Note: this wouldn't work if /data didn't load the session. self.getPage('/data') - self.assertBody("{'aha': 'foo'}") + assert self.body == b'{"aha": "foo"}' c = self.cookies[0] self.getPage('/data', self.cookies) - self.assertEqual(self.cookies[0], c) + self.cookies[0] == c self.getPage('/testStr') - self.assertBody('1') + assert self.body == b'1' cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(';')]) # Assert there is an 'expires' param - self.assertEqual(set(cookie_parts.keys()), - set(['session_id', 'expires', 'Path'])) + expected_cookie_keys = {'session_id', 'expires', 'Path', 'Max-Age'} + assert set(cookie_parts.keys()) == expected_cookie_keys self.getPage('/testGen', self.cookies) - self.assertBody('2') + assert self.body == b'2' self.getPage('/testStr', self.cookies) - self.assertBody('3') + assert self.body == b'3' self.getPage('/data', self.cookies) - self.assertDictEqual(json_decode(self.body), - {'counter': 3, 'aha': 'foo'}) + expected_data = {'counter': 3, 'aha': 'foo'} + assert json.decode(self.body.decode('utf-8')) == expected_data self.getPage('/length', self.cookies) - self.assertBody('2') + assert self.body == b'2' self.getPage('/delkey?key=counter', self.cookies) - self.assertStatus(200) + assert self.status_code == 200 self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession') self.getPage('/testStr') - self.assertBody('1') + assert self.body == b'1' self.getPage('/testGen', self.cookies) - self.assertBody('2') + assert self.body == b'2' self.getPage('/testStr', self.cookies) - self.assertBody('3') + assert self.body == b'3' self.getPage('/delkey?key=counter', self.cookies) - self.assertStatus(200) + assert self.status_code == 200 # Wait for the session.timeout (1 second) time.sleep(2) self.getPage('/') - self.assertBody('1') + assert self.body == b'1' self.getPage('/length', self.cookies) - self.assertBody('1') + assert self.body == b'1' # Test session __contains__ self.getPage('/keyin?key=counter', self.cookies) - self.assertBody('True') + assert self.body == b'True' cookieset1 = self.cookies # Make a new session and test __len__ again self.getPage('/') self.getPage('/length', self.cookies) - self.assertBody('2') + assert self.body == b'2' # Test session delete self.getPage('/delete', self.cookies) - self.assertBody('done') + assert self.body == b'done' self.getPage('/delete', cookieset1) - self.assertBody('done') + assert self.body == b'done' def f(): return [ @@ -220,13 +224,13 @@ class SessionTest(helper.CPWebCase): for x in os.listdir(localDir) if x.startswith('session-') ] - self.assertEqual(f(), []) + assert f() == [] # Wait for the cleanup thread to delete remaining session files self.getPage('/') - self.assertNotEqual(f(), []) + assert f() != [] time.sleep(2) - self.assertEqual(f(), []) + assert f() == [] def test_1_Ram_Concurrency(self): self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession') @@ -243,7 +247,7 @@ class SessionTest(helper.CPWebCase): # Get initial cookie self.getPage('/') - self.assertBody('1') + assert self.body == b'1' cookies = self.cookies data_dict = {} @@ -285,13 +289,14 @@ class SessionTest(helper.CPWebCase): for e in errors: print(e) - self.assertEqual(hitcount, expected) + assert len(errors) == 0 + assert hitcount == expected def test_3_Redirect(self): # Start a new session self.getPage('/testStr') self.getPage('/iredir', self.cookies) - self.assertBody('FileSession') + assert self.body == b'FileSession' def test_4_File_deletion(self): # Start a new session @@ -319,9 +324,9 @@ class SessionTest(helper.CPWebCase): # grab the cookie ID id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] self.getPage('/regen') - self.assertBody('logged in') + assert self.body == b'logged in' id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] - self.assertNotEqual(id1, id2) + assert id1 != id2 self.getPage('/testStr') # grab the cookie ID @@ -332,8 +337,8 @@ class SessionTest(helper.CPWebCase): 'session_id=maliciousid; ' 'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')]) id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] - self.assertNotEqual(id1, id2) - self.assertNotEqual(id2, 'maliciousid') + assert id1 != id2 + assert id2 != 'maliciousid' def test_7_session_cookies(self): self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession') @@ -343,18 +348,18 @@ class SessionTest(helper.CPWebCase): cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(';')]) # Assert there is no 'expires' param - self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + assert set(cookie_parts.keys()) == {'temp', 'Path'} id1 = cookie_parts['temp'] - self.assertEqual(list(sessions.RamSession.cache), [id1]) + assert list(sessions.RamSession.cache) == [id1] # Send another request in the same "browser session". self.getPage('/session_cookie', self.cookies) cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(';')]) # Assert there is no 'expires' param - self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) - self.assertBody(id1) - self.assertEqual(list(sessions.RamSession.cache), [id1]) + assert set(cookie_parts.keys()) == {'temp', 'Path'} + assert self.body.decode('utf-8') == id1 + assert list(sessions.RamSession.cache) == [id1] # Simulate a browser close by just not sending the cookies self.getPage('/session_cookie') @@ -362,12 +367,11 @@ class SessionTest(helper.CPWebCase): cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(';')]) # Assert there is no 'expires' param - self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + assert set(cookie_parts.keys()) == {'temp', 'Path'} # Assert a new id has been generated... id2 = cookie_parts['temp'] - self.assertNotEqual(id1, id2) - self.assertEqual(set(sessions.RamSession.cache.keys()), - set([id1, id2])) + assert id1 != id2 + assert set(sessions.RamSession.cache.keys()) == {id1, id2} # Wait for the session.timeout on both sessions time.sleep(2.5) @@ -398,115 +402,147 @@ class SessionTest(helper.CPWebCase): t.join() -try: - importlib.import_module('memcache') +def is_memcached_present(): + executable = find_executable('memcached') + return bool(executable) + + +@pytest.fixture(scope='session') +def memcached_server_present(): + is_memcached_present() or pytest.skip('memcached not available') + + +@pytest.fixture() +def memcached_client_present(): + pytest.importorskip('memcache') + + +@pytest.fixture(scope='session') +def memcached_instance(request, watcher_getter, memcached_server_present): + """ + Start up an instance of memcached. + """ + + port = portend.find_available_local_port() - host, port = '127.0.0.1', 11211 - for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - s = None + def is_occupied(): try: - s = socket.socket(af, socktype, proto) - # See http://groups.google.com/group/cherrypy-users/ - # browse_frm/thread/bbfe5eb39c904fe0 - s.settimeout(1.0) - s.connect((host, port)) - s.close() - except socket.error: - if s: - s.close() - raise - break -except (ImportError, socket.error): - class MemcachedSessionTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test(self): - return self.skip('memcached not reachable ') -else: - class MemcachedSessionTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_0_Session(self): - self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession') - - self.getPage('/testStr') - self.assertBody('1') - self.getPage('/testGen', self.cookies) - self.assertBody('2') - self.getPage('/testStr', self.cookies) - self.assertBody('3') - self.getPage('/length', self.cookies) - self.assertErrorPage(500) - self.assertInBody('NotImplementedError') - self.getPage('/delkey?key=counter', self.cookies) - self.assertStatus(200) - - # Wait for the session.timeout (1 second) - time.sleep(1.25) - self.getPage('/') - self.assertBody('1') - - # Test session __contains__ - self.getPage('/keyin?key=counter', self.cookies) - self.assertBody('True') - - # Test session delete - self.getPage('/delete', self.cookies) - self.assertBody('done') - - def test_1_Concurrency(self): - client_thread_count = 5 - request_count = 30 - - # Get initial cookie - self.getPage('/') - self.assertBody('1') - cookies = self.cookies - - data_dict = {} - - def request(index): - for i in range(request_count): - self.getPage('/', cookies) - # Uncomment the following line to prove threads overlap. - # sys.stdout.write("%d " % index) - if not self.body.isdigit(): - self.fail(self.body) - data_dict[index] = int(self.body) - - # Start <request_count> concurrent requests from - # each of <client_thread_count> clients - ts = [] - for c in range(client_thread_count): - data_dict[c] = 0 - t = threading.Thread(target=request, args=(c,)) - ts.append(t) - t.start() - - for t in ts: - t.join() - - hitcount = max(data_dict.values()) - expected = 1 + (client_thread_count * request_count) - self.assertEqual(hitcount, expected) - - def test_3_Redirect(self): - # Start a new session - self.getPage('/testStr') - self.getPage('/iredir', self.cookies) - self.assertBody('memcached') - - def test_5_Error_paths(self): - self.getPage('/unknown/page') - self.assertErrorPage( - 404, "The path '/unknown/page' was not found.") - - # Note: this path is *not* the same as above. The above - # takes a normal route through the session code; this one - # skips the session code's before_handler and only calls - # before_finalize (save) and on_end (close). So the session - # code has to survive calling save/close without init. - self.getPage('/restricted', self.cookies, method='POST') - self.assertErrorPage(405, response_codes[405][1]) + portend.Checker().assert_free('localhost', port) + except Exception: + return True + return False + + proc = watcher_getter( + name='memcached', + arguments=['-p', str(port)], + checker=is_occupied, + request=request, + ) + return locals() + + +@pytest.fixture +def memcached_configured( + memcached_instance, monkeypatch, + memcached_client_present, +): + server = 'localhost:{port}'.format_map(memcached_instance) + monkeypatch.setattr( + sessions.MemcachedSession, + 'servers', + [server], + ) + + +@pytest.mark.skipif( + platform.system() == 'Windows', + reason='pytest-services helper does not work under Windows', +) +@pytest.mark.usefixtures('memcached_configured') +class MemcachedSessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_0_Session(self): + self.getPage( + '/set_session_cls/cherrypy.lib.sessions.MemcachedSession' + ) + + self.getPage('/testStr') + assert self.body == b'1' + self.getPage('/testGen', self.cookies) + assert self.body == b'2' + self.getPage('/testStr', self.cookies) + assert self.body == b'3' + self.getPage('/length', self.cookies) + self.assertErrorPage(500) + assert b'NotImplementedError' in self.body + self.getPage('/delkey?key=counter', self.cookies) + assert self.status_code == 200 + + # Wait for the session.timeout (1 second) + time.sleep(1.25) + self.getPage('/') + assert self.body == b'1' + + # Test session __contains__ + self.getPage('/keyin?key=counter', self.cookies) + assert self.body == b'True' + + # Test session delete + self.getPage('/delete', self.cookies) + assert self.body == b'done' + + def test_1_Concurrency(self): + client_thread_count = 5 + request_count = 30 + + # Get initial cookie + self.getPage('/') + assert self.body == b'1' + cookies = self.cookies + + data_dict = {} + + def request(index): + for i in range(request_count): + self.getPage('/', cookies) + # Uncomment the following line to prove threads overlap. + # sys.stdout.write("%d " % index) + if not self.body.isdigit(): + self.fail(self.body) + data_dict[index] = int(self.body) + + # Start <request_count> concurrent requests from + # each of <client_thread_count> clients + ts = [] + for c in range(client_thread_count): + data_dict[c] = 0 + t = threading.Thread(target=request, args=(c,)) + ts.append(t) + t.start() + + for t in ts: + t.join() + + hitcount = max(data_dict.values()) + expected = 1 + (client_thread_count * request_count) + assert hitcount == expected + + def test_3_Redirect(self): + # Start a new session + self.getPage('/testStr') + self.getPage('/iredir', self.cookies) + assert self.body == b'MemcachedSession' + + def test_5_Error_paths(self): + self.getPage('/unknown/page') + self.assertErrorPage( + 404, "The path '/unknown/page' was not found.") + + # Note: this path is *not* the same as above. The above + # takes a normal route through the session code; this one + # skips the session code's before_handler and only calls + # before_finalize (save) and on_end (close). So the session + # code has to survive calling save/close without init. + self.getPage('/restricted', self.cookies, method='POST') + self.assertErrorPage(405, response_codes[405][1]) diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py index 606ca4f6..a5addbca 100644 --- a/cherrypy/test/test_states.py +++ b/cherrypy/test/test_states.py @@ -3,8 +3,7 @@ import signal import time import unittest import warnings - -from six.moves.http_client import BadStatusLine +from http.client import BadStatusLine import pytest import portend diff --git a/cherrypy/test/test_static.py b/cherrypy/test/test_static.py index 52f4006f..cdd821ae 100644 --- a/cherrypy/test/test_static.py +++ b/cherrypy/test/test_static.py @@ -1,17 +1,15 @@ # -*- coding: utf-8 -*- -import contextlib import io import os import sys import platform import tempfile - -from six import text_type as str -from six.moves import urllib -from six.moves.http_client import HTTPConnection +import urllib.parse +from http.client import HTTPConnection import pytest import py.path +import path import cherrypy from cherrypy.lib import static @@ -46,9 +44,9 @@ def ensure_unicode_filesystem(): tmpdir.remove() -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -has_space_filepath = os.path.join(curdir, 'static', 'has space.html') -bigfile_filepath = os.path.join(curdir, 'static', 'bigfile.log') +curdir = path.Path(__file__).dirname() +has_space_filepath = curdir / 'static' / 'has space.html' +bigfile_filepath = curdir / 'static' / 'bigfile.log' # The file size needs to be big enough such that half the size of it # won't be socket-buffered (or server-buffered) all in one go. See @@ -58,6 +56,7 @@ BIGFILE_SIZE = 32 * MB class StaticTest(helper.CPWebCase): + files_to_remove = [] @staticmethod def setup_server(): @@ -157,14 +156,13 @@ class StaticTest(helper.CPWebCase): vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp}) cherrypy.tree.graft(vhost) - @staticmethod - def teardown_server(): - for f in (has_space_filepath, bigfile_filepath): - if os.path.exists(f): - try: - os.unlink(f) - except Exception: - pass + @classmethod + def teardown_class(cls): + super(cls, cls).teardown_class() + files_to_remove = has_space_filepath, bigfile_filepath + files_to_remove += tuple(cls.files_to_remove) + for file in files_to_remove: + file.remove_p() def test_static(self): self.getPage('/static/index.html') @@ -403,34 +401,25 @@ class StaticTest(helper.CPWebCase): self.getPage('/static/\x00') self.assertStatus('404 Not Found') - @staticmethod - @contextlib.contextmanager - def unicode_file(): + @classmethod + def unicode_file(cls): filename = ntou('Слава Україні.html', 'utf-8') - filepath = os.path.join(curdir, 'static', filename) - with io.open(filepath, 'w', encoding='utf-8') as strm: + filepath = curdir / 'static' / filename + with filepath.open('w', encoding='utf-8')as strm: strm.write(ntou('Героям Слава!', 'utf-8')) - try: - yield - finally: - os.remove(filepath) - - py27_on_windows = ( - platform.system() == 'Windows' and - sys.version_info < (3,) - ) - @pytest.mark.xfail(py27_on_windows, reason='#1544') # noqa: E301 + cls.files_to_remove.append(filepath) + def test_unicode(self): ensure_unicode_filesystem() - with self.unicode_file(): - url = ntou('/static/Слава Україні.html', 'utf-8') - # quote function requires str - url = tonative(url, 'utf-8') - url = urllib.parse.quote(url) - self.getPage(url) - - expected = ntou('Героям Слава!', 'utf-8') - self.assertInBody(expected) + self.unicode_file() + url = ntou('/static/Слава Україні.html', 'utf-8') + # quote function requires str + url = tonative(url, 'utf-8') + url = urllib.parse.quote(url) + self.getPage(url) + + expected = ntou('Героям Слава!', 'utf-8') + self.assertInBody(expected) def error_page_404(status, message, traceback, version): diff --git a/cherrypy/test/test_tools.py b/cherrypy/test/test_tools.py index a73a3898..3a0fd389 100644 --- a/cherrypy/test/test_tools.py +++ b/cherrypy/test/test_tools.py @@ -7,10 +7,7 @@ import time import types import unittest import operator - -import six -from six.moves import range, map -from six.moves.http_client import IncompleteRead +from http.client import IncompleteRead import cherrypy from cherrypy import tools @@ -52,7 +49,7 @@ class ToolTests(helper.CPWebCase): def _setup(self): def makemap(): m = self._merged_args().get('map', {}) - cherrypy.request.numerify_map = list(six.iteritems(m)) + cherrypy.request.numerify_map = list(m.items()) cherrypy.request.hooks.attach('on_start_resource', makemap) def critical(): @@ -105,10 +102,7 @@ class ToolTests(helper.CPWebCase): def __call__(self, scale): r = cherrypy.response r.collapse_body() - if six.PY3: - r.body = [bytes([(x + scale) % 256 for x in r.body[0]])] - else: - r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]] + r.body = [bytes([(x + scale) % 256 for x in r.body[0]])] cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator()) def stream_handler(next_handler, *args, **kwargs): @@ -179,7 +173,7 @@ class ToolTests(helper.CPWebCase): """ def __init__(cls, name, bases, dct): type.__init__(cls, name, bases, dct) - for value in six.itervalues(dct): + for value in dct.values(): if isinstance(value, types.FunctionType): cherrypy.expose(value) setattr(root, name.lower(), cls()) @@ -346,7 +340,7 @@ class ToolTests(helper.CPWebCase): self.getPage('/demo/err_in_onstart') self.assertErrorPage(502) tmpl = "AttributeError: 'str' object has no attribute '{attr}'" - expected_msg = tmpl.format(attr='items' if six.PY3 else 'iteritems') + expected_msg = tmpl.format(attr='items') self.assertInBody(expected_msg) def testCombinedTools(self): @@ -377,11 +371,7 @@ class ToolTests(helper.CPWebCase): # but it proves the priority was changed. self.getPage('/decorated_euro/subpath', headers=[('Accept-Encoding', 'gzip')]) - if six.PY3: - self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()])) - else: - self.assertInBody(''.join([chr((ord(x) + 3) % 256) - for x in zbuf.getvalue()])) + self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()])) def testBareHooks(self): content = 'bit of a pain in me gulliver' @@ -446,8 +436,8 @@ class SessionAuthTest(unittest.TestCase): username and password were unicode. """ sa = cherrypy.lib.cptools.SessionAuth() - res = sa.login_screen(None, username=six.text_type('nobody'), - password=six.text_type('anypass')) + res = sa.login_screen(None, username=str('nobody'), + password=str('anypass')) self.assertTrue(isinstance(res, bytes)) diff --git a/cherrypy/test/test_tutorials.py b/cherrypy/test/test_tutorials.py index efa35b99..002a2b45 100644 --- a/cherrypy/test/test_tutorials.py +++ b/cherrypy/test/test_tutorials.py @@ -1,10 +1,7 @@ import sys import imp -import types import importlib -import six - import cherrypy from cherrypy.test import helper @@ -39,8 +36,6 @@ class TutorialTest(helper.CPWebCase): root = getattr(module, root_name) conf = getattr(module, 'tutconf') class_types = type, - if six.PY2: - class_types += types.ClassType, if isinstance(root, class_types): root = root() cherrypy.tree.mount(root, config=conf) diff --git a/cherrypy/test/test_wsgi_unix_socket.py b/cherrypy/test/test_wsgi_unix_socket.py index 8f1cc00b..df0ab5f8 100644 --- a/cherrypy/test/test_wsgi_unix_socket.py +++ b/cherrypy/test/test_wsgi_unix_socket.py @@ -2,8 +2,7 @@ import os import socket import atexit import tempfile - -from six.moves.http_client import HTTPConnection +from http.client import HTTPConnection import pytest diff --git a/cherrypy/test/test_xmlrpc.py b/cherrypy/test/test_xmlrpc.py index ad93b821..61fde8bb 100644 --- a/cherrypy/test/test_xmlrpc.py +++ b/cherrypy/test/test_xmlrpc.py @@ -1,54 +1,20 @@ import sys +import socket -import six - -from six.moves.xmlrpc_client import ( +from xmlrpc.client import ( DateTime, Fault, - ProtocolError, ServerProxy, SafeTransport + ServerProxy, SafeTransport ) import cherrypy from cherrypy import _cptools from cherrypy.test import helper -if six.PY3: - HTTPSTransport = SafeTransport - - # Python 3.0's SafeTransport still mistakenly checks for socket.ssl - import socket - if not hasattr(socket, 'ssl'): - socket.ssl = True -else: - class HTTPSTransport(SafeTransport): - - """Subclass of SafeTransport to fix sock.recv errors (by using file). - """ - - def request(self, host, handler, request_body, verbose=0): - # issue XML-RPC request - h = self.make_connection(host) - if verbose: - h.set_debuglevel(1) - - self.send_request(h, handler, request_body) - self.send_host(h, host) - self.send_user_agent(h) - self.send_content(h, request_body) - - errcode, errmsg, headers = h.getreply() - if errcode != 200: - raise ProtocolError(host + handler, errcode, errmsg, headers) - - self.verbose = verbose - - # Here's where we differ from the superclass. It says: - # try: - # sock = h._conn.sock - # except AttributeError: - # sock = None - # return self._parse_response(h.getfile(), sock) +HTTPSTransport = SafeTransport - return self.parse_response(h.getfile()) +# Python 3.0's SafeTransport still mistakenly checks for socket.ssl +if not hasattr(socket, 'ssl'): + socket.ssl = True def setup_server(): diff --git a/docs/_templates/python_2_eol.html b/docs/_templates/python_2_eol.html index 21ae4333..b0f87888 100644 --- a/docs/_templates/python_2_eol.html +++ b/docs/_templates/python_2_eol.html @@ -4,16 +4,23 @@ Python 3, the new best practice, is here to stay. Python 2 will retire in only <time></time>! </a> + <p> + Since version 18.0.0 CherryPy has + <a href="https://python3statement.org" target="_blank">dropped + support for Python 2</a>, + but there's still LTS branch for v17 supporting hybrid + Python 2 and 3 code, which will get bugfixes and security updates. + </p> </aside> <!-- Python 2 EOL Clock --> <style type="text/css"> body { - margin-top: 4em; + margin-top: 8em; } #python27eol { - position: fixed; + position: absolute; top: 0; left: 0; right: 0; height: auto; @@ -21,10 +28,9 @@ color: white; background-color: black; font-size: larger; - line-height: 3; } - #python27eol>* { + #python27eol * { color: white; background-color: black; } @@ -32,6 +38,41 @@ #python27eol>a { display: block; } + + #python27eol>p { + padding: 0 100px; + } + + @media screen and (max-width: 875px) { + #python27eol p { + padding: 0 !important; + } + + body { + margin: inherit !important; + padding: inherit !important; + } + + div.body { + min-width: inherit !important; + width: 100% !important; + } + + div.documentwrapper { + margin: inherit !important; + } + + div.bodywrapper { + margin: 20px 30px !important; + padding-top: 190px !important; + } + + div.sphinxsidebar { + margin: inherit !important; + padding: inherit !important; + width: 100% !important; + } + } </style> <script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.min.js"></script> @@ -39,9 +80,9 @@ <script src="//cdn.rawgit.com/icambron/moment-countdown/master/dist/moment-countdown.min.js"></script> <script> - var py2_death = moment('2020-04-12').toDate() + var py2_death = moment('2020-01-01').utc().toDate() - countdown(py2_death, update_time_left) + countdown(py2_death, update_time_left, ~(countdown.SECONDS | countdown.MILLISECONDS)) function update_time_left(moments_left, timer_id) { document.querySelector('#python27eol time').innerHTML = moments_left.toHTML("em") diff --git a/docs/advanced.rst b/docs/advanced.rst index 3def3437..97a5892a 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -405,7 +405,7 @@ and wrap your entire CherryPy application with it: headers = cherrypy.response.headers headers['X-Frame-Options'] = 'DENY' headers['X-XSS-Protection'] = '1; mode=block' - headers['Content-Security-Policy'] = "default-src='self'" + headers['Content-Security-Policy'] = "default-src 'self';" .. note:: @@ -561,12 +561,12 @@ is provided by an external library called Database support ################ -CherryPy does not bundle any database access but its architecture -makes it easy to integrate common database interfaces such as -the DB-API specified in :pep:`249`. Alternatively, you can also -use an `ORM <en.wikipedia.org/wiki/Object-relational_mapping>`_ -such as `SQLAlchemy <http://sqlalchemy.readthedocs.org>`_ -or `SQLObject <https://pypi.python.org/pypi/SQLObject/>`_. +CherryPy does not bundle any database access but its architecture makes it easy +to integrate common database interfaces such as the DB-API specified in +:pep:`249`. Alternatively, you can also use an `ORM +<https://en.wikipedia.org/wiki/Object-relational_mapping>`_ such as `SQLAlchemy +<http://sqlalchemy.readthedocs.org>`_ or `SQLObject +<https://pypi.python.org/pypi/SQLObject/>`_. You will find `here <https://bitbucket.org/Lawouach/cherrypy-recipes/src/tip/web/database/sql_alchemy/>`_ a recipe on how integrating SQLAlchemy using a mix of diff --git a/docs/basics.rst b/docs/basics.rst index 717eedbe..4f2bc83e 100644 --- a/docs/basics.rst +++ b/docs/basics.rst @@ -839,7 +839,7 @@ sockets. This is how you enable it: ``server.peercreds`` enables looking up the connected process ID, user ID and group ID. They'll be accessible as WSGI environment -variables:: +variables: * ``X_REMOTE_PID`` @@ -848,7 +848,7 @@ variables:: * ``X_REMOTE_GID`` ``server.peercreds_resolve`` resolves that into user name and group -name. They'll be accessible as WSGI environment variables:: +name. They'll be accessible as WSGI environment variables: * ``X_REMOTE_USER`` and ``REMOTE_USER`` diff --git a/docs/conf.py b/docs/conf.py index bad773be..7c600b48 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -149,7 +149,7 @@ html_theme_options = { 'github_repo': project.lower(), 'github_button': True, 'github_banner': True, - 'github_type': 'watch', + 'github_type': 'star', 'github_count': True, 'travis_button': True, 'codecov_button': True, diff --git a/docs/index.rst b/docs/index.rst index 0b6f6319..e4ef2e16 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,5 +1,5 @@ -CherryPy - A Minimalist Python Web Framework +CherryPy — A Minimalist Python Web Framework ============================================ .. toctree:: diff --git a/docs/tutorials.rst b/docs/tutorials.rst index c25cf747..25db5cdf 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -76,6 +76,8 @@ log is harmless and will not prevent CherryPy from working. You can refer to :ref:`the documentation above <perappconf>` to understand how to set the configuration. +.. _tut02: + Tutorial 2: Different URLs lead to different functions ###################################################### @@ -1198,3 +1200,96 @@ those to a database for instance), etc. :ref:`Plugins <busplugins>` are called that way because they work along with the CherryPy :ref:`engine <cpengine>` and extend it with your operations. + +Tutorial 12: Using pytest and code coverage +########################################### + +Pytest +^^^^^^ +Let's revisit :ref:`Tutorial 2 <tut02>`. + +.. code-block:: python + :linenos: + + import random + import string + + import cherrypy + + + class StringGenerator(object): + @cherrypy.expose + def index(self): + return "Hello world!" + + @cherrypy.expose + def generate(self): + return ''.join(random.sample(string.hexdigits, 8)) + + + if __name__ == '__main__': + cherrypy.quickstart(StringGenerator()) + +Save this into a file named ``tut12.py``. + +Now make the test file: + +.. code-block:: python + :linenos: + + import cherrypy + from cherrypy.test import helper + + from tut12 import StringGenerator + + class SimpleCPTest(helper.CPWebCase): + @staticmethod + def setup_server(): + cherrypy.tree.mount(StringGenerator(), '/', {}) + + def test_index(self): + self.getPage("/") + self.assertStatus('200 OK') + def test_generate(self): + self.getPage("/generate") + self.assertStatus('200 OK') + +Save this into a file named ``test_tut12.py`` and run + +.. code-block:: bash + + $ pytest -v test_tut12.py + +.. note:: + + If you don't have `pytest <https://pytest.org>`_ installed, you'll need to install it by ``pip install pytest`` + +We now have a neat way that we can exercise our application making tests. + +Adding Code Coverage +^^^^^^^^^^^^^^^^^^^^ + +To get code coverage, simply run + +.. code-block:: bash + + $ pytest --cov=tut12 --cov-report term-missing test_tut12.py + +.. note:: + + To `add coverage support to pytest <https://pytest-cov.rtfd.io>`_, you'll need to install it by ``pip install pytest-cov`` + +This tells us that one line is missing. Of course it is because that is only executed when +the python program is started directly. We can simply change the following lines in ``tut12.py``: + +.. code-block:: python + :lineno-start: 17 + + if __name__ == '__main__': # pragma: no cover + cherrypy.quickstart(StringGenerator()) + +When you rerun the code coverage, it should show 100% now. + +.. note:: + + When using in CI, you might want to integrate `Codecov <https://codecov.io>`_, `Landscape <https://landscape.io>`_ or `Coveralls <https://coveralls.io/>`_ into your project to store and track coverage data over time. @@ -1,4 +1,6 @@ [pytest] -norecursedirs=dist build .tox .eggs -addopts=-v -rxs --junitxml=.test-results/pytest/results.xml --cov=cherrypy --cov-report term-missing:skip-covered --cov-report xml --doctest-modules -doctest_optionflags=ALLOW_UNICODE ELLIPSIS +norecursedirs = dist build .tox .eggs +addopts = -v -rxXs --junitxml=.test-results/pytest/results.xml --cov=cherrypy --cov-report term-missing:skip-covered --cov-report xml --doctest-modules +doctest_optionflags = ALLOW_UNICODE ELLIPSIS +junit_suite_name = cherrypy_test_suite +testpaths = cherrypy/test/ @@ -24,10 +24,7 @@ params = dict( 'Framework :: CherryPy', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', @@ -61,12 +58,10 @@ params = dict( entry_points={'console_scripts': ['cherryd = cherrypy.__main__:run']}, include_package_data=True, install_requires=[ - 'six>=1.11.0', 'cheroot>=6.2.4', 'portend>=2.1.1', 'more_itertools', 'zc.lockfile', - 'contextlib2', ], extras_require={ 'docs': [ @@ -93,19 +88,20 @@ params = dict( 'path.py', 'requests_toolbelt', ], + 'testing:sys_platform != "win32"': [ + 'pytest-services', + ], # Enables memcached session support via `cherrypy[memcached_session]`: 'memcached_session': ['python-memcached>=1.58'], 'xcgi': ['flup'], # https://docs.cherrypy.org/en/latest/advanced.html?highlight=windows#windows-console-events - ':sys_platform == "win32" and python_version != "3.4"': ['pywin32'], - ':sys_platform == "win32" and python_version == "3.4"': - ['pypiwin32==219'], + ':sys_platform == "win32"': ['pywin32'], }, setup_requires=[ 'setuptools_scm', ], - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', + python_requires='>=3.5', ) diff --git a/tests/dist-check.py b/tests/dist-check.py index 3f1cf0fa..008a63d2 100644 --- a/tests/dist-check.py +++ b/tests/dist-check.py @@ -12,8 +12,6 @@ suite because it must import cherrypy late (after removing sys.path[0]). """ -from __future__ import print_function - import os import sys @@ -3,9 +3,13 @@ envlist = python minversion = 2.4 [testenv] +# Hotfix for https://github.com/pypa/pip/issues/6434 +# Based on https://github.com/jaraco/skeleton/commit/123b0b2 +# Check https://github.com/tox-dev/tox/issues/1276 for the final solution +install_command = + python -c 'import subprocess, sys; pip_inst_cmd = sys.executable, "-m", "pip", "install"; subprocess.check_call(pip_inst_cmd + ("pip<19.1", )); subprocess.check_call(pip_inst_cmd + tuple(sys.argv[1:]))' {opts} {packages} usedevelop = True commands = - mkdir -p {toxinidir}/.test-results/pytest pytest {posargs} codecov -f coverage.xml -X gcov passenv = @@ -22,6 +26,7 @@ setenv = extras = testing routes_dispatcher + memcached_session whitelist_externals = mkdir [testenv:cheroot-master] |