summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.appveyor.yml (renamed from appveyor.yml)5
-rw-r--r--.circleci/config.yml14
-rw-r--r--.github/ISSUE_TEMPLATE.md16
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md12
-rw-r--r--.github/config.yml2
-rw-r--r--.gitignore2
-rw-r--r--.test-results/pytest/.gitignore1
-rw-r--r--.travis.yml27
-rw-r--r--CHANGES.rst34
-rw-r--r--README.rst11
-rw-r--r--cherrypy/_cpchecker.py10
-rw-r--r--cherrypy/_cpcompat.py157
-rw-r--r--cherrypy/_cperror.py16
-rw-r--r--cherrypy/_cplogging.py69
-rw-r--r--cherrypy/_cpmodpy.py4
-rw-r--r--cherrypy/_cpreqbody.py39
-rw-r--r--cherrypy/_cprequest.py20
-rw-r--r--cherrypy/_cpserver.py23
-rw-r--r--cherrypy/_cptools.py13
-rw-r--r--cherrypy/_cptree.py15
-rw-r--r--cherrypy/_cpwsgi.py22
-rw-r--r--cherrypy/_helper.py6
-rw-r--r--cherrypy/_json.py25
-rw-r--r--cherrypy/lib/auth_digest.py3
-rw-r--r--cherrypy/lib/caching.py9
-rw-r--r--cherrypy/lib/covercp.py3
-rw-r--r--cherrypy/lib/cpstats.py6
-rw-r--r--cherrypy/lib/cptools.py6
-rw-r--r--cherrypy/lib/encoding.py8
-rw-r--r--cherrypy/lib/httputil.py42
-rw-r--r--cherrypy/lib/jsontools.py7
-rw-r--r--cherrypy/lib/reprconf.py139
-rw-r--r--cherrypy/lib/sessions.py16
-rw-r--r--cherrypy/lib/static.py3
-rw-r--r--cherrypy/lib/xmlrpcutil.py3
-rw-r--r--cherrypy/process/plugins.py12
-rw-r--r--cherrypy/process/win32.py8
-rw-r--r--cherrypy/process/wspbus.py9
-rw-r--r--cherrypy/test/helper.py25
-rw-r--r--cherrypy/test/logtest.py14
-rwxr-xr-xcherrypy/test/sessiondemo.py4
-rw-r--r--cherrypy/test/test_auth_digest.py5
-rw-r--r--cherrypy/test/test_bus.py28
-rw-r--r--cherrypy/test/test_caching.py4
-rw-r--r--cherrypy/test/test_compat.py34
-rw-r--r--cherrypy/test/test_config.py16
-rw-r--r--cherrypy/test/test_conn.py11
-rw-r--r--cherrypy/test/test_core.py4
-rw-r--r--cherrypy/test/test_dynamicobjectmapping.py8
-rw-r--r--cherrypy/test/test_encoding.py5
-rw-r--r--cherrypy/test/test_http.py16
-rw-r--r--cherrypy/test/test_httputil.py6
-rw-r--r--cherrypy/test/test_iterator.py6
-rw-r--r--cherrypy/test/test_json.py3
-rw-r--r--cherrypy/test/test_logging.py70
-rw-r--r--cherrypy/test/test_refleaks.py3
-rw-r--r--cherrypy/test/test_request_obj.py8
-rwxr-xr-xcherrypy/test/test_session.py370
-rw-r--r--cherrypy/test/test_states.py3
-rw-r--r--cherrypy/test/test_static.py69
-rw-r--r--cherrypy/test/test_tools.py26
-rw-r--r--cherrypy/test/test_tutorials.py5
-rw-r--r--cherrypy/test/test_wsgi_unix_socket.py3
-rw-r--r--cherrypy/test/test_xmlrpc.py48
-rw-r--r--docs/_templates/python_2_eol.html53
-rw-r--r--docs/advanced.rst14
-rw-r--r--docs/basics.rst4
-rw-r--r--docs/conf.py2
-rw-r--r--docs/index.rst2
-rw-r--r--docs/tutorials.rst95
-rw-r--r--pytest.ini8
-rwxr-xr-xsetup.py14
-rw-r--r--tests/dist-check.py2
-rw-r--r--tox.ini7
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
diff --git a/.gitignore b/.gitignore
index 6ffd1573..e0e454b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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)
--------------------
diff --git a/README.rst b/README.rst
index 385f23f7..6b97d653 100644
--- a/README.rst
+++ b/README.rst
@@ -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&amp;&lt;&gt;"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.
diff --git a/pytest.ini b/pytest.ini
index 2d6ef670..857890f8 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -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/
diff --git a/setup.py b/setup.py
index c2ee1d2d..a3ebd20a 100755
--- a/setup.py
+++ b/setup.py
@@ -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
diff --git a/tox.ini b/tox.ini
index 2bb1ae44..59f7ce55 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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]