From 3bae7f06868553b006915f05ff14d86163f59a7d Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sun, 17 Jan 2021 02:51:53 +0100 Subject: [PATCH 001/322] Ignore pytest 6.2.0 resource warnings under py 3.8 Refs: * #1897 * https://docs.pytest.org/en/stable/usage.html#unraisable * https://github.com/pytest-dev/pytest/issues/5299 --- pytest.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pytest.ini b/pytest.ini index 909f146c5..89197092d 100644 --- a/pytest.ini +++ b/pytest.ini @@ -33,6 +33,13 @@ addopts = doctest_optionflags = ALLOW_UNICODE ELLIPSIS filterwarnings = error + + # pytest>=6.2.0 under Python 3.8: + # Ref: https://docs.pytest.org/en/stable/usage.html#unraisable + # Ref: https://github.com/pytest-dev/pytest/issues/5299 + ignore:Exception ignored in. :pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception + ignore:Exception ignored in. <_io.FileIO .closed.>:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception + ignore:Use cheroot.test.webtest:DeprecationWarning ignore:This method will be removed in future versions.*:DeprecationWarning ignore:Unable to verify that the server is bound on:UserWarning From 59c0e19d7df8680e36afc96756dce72435121448 Mon Sep 17 00:00:00 2001 From: Duncan Bellamy Date: Sun, 17 Jan 2021 03:02:24 +0100 Subject: [PATCH 002/322] Close streamed file in file_generator on destruct --- cherrypy/lib/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cherrypy/lib/__init__.py b/cherrypy/lib/__init__.py index f815f76ad..0edaaf20c 100644 --- a/cherrypy/lib/__init__.py +++ b/cherrypy/lib/__init__.py @@ -70,6 +70,11 @@ def __next__(self): raise StopIteration() next = __next__ + def __del__(self): + """Close input on descturct.""" + if hasattr(self.input, 'close'): + self.input.close() + def file_generator_limited(fileobj, count, chunk_size=65536): """Yield the given file object in chunks. From 4a6287b73539adcb7b0ae72d69644a1ced1f7eaa Mon Sep 17 00:00:00 2001 From: Duncan Bellamy Date: Fri, 15 Jan 2021 14:49:09 +0000 Subject: [PATCH 003/322] Close hanging fd in testHandlerToolConfigOverride --- cherrypy/test/test_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cherrypy/test/test_config.py b/cherrypy/test/test_config.py index 5e880d873..ecd460198 100644 --- a/cherrypy/test/test_config.py +++ b/cherrypy/test/test_config.py @@ -221,8 +221,8 @@ def testHandlerToolConfigOverride(self): # the favicon in the page handler to be '../favicon.ico', # but then overrode it in config to be './static/dirback.jpg'. self.getPage('/favicon.ico') - self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'), - 'rb').read()) + with open(os.path.join(localDir, 'static/dirback.jpg'), 'rb') as tf: + self.assertBody(tf.read()) def test_request_body_namespace(self): self.getPage('/plain', method='POST', headers=[ From 98929b519fbca003cbf7b14a6b370a3cabc9c412 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Mon, 18 Jan 2021 00:39:22 +0100 Subject: [PATCH 004/322] Autogenerate module docs with sphinxcontrib-apidoc --- docs/conf.py | 18 ++ docs/index.rst | 7 +- docs/pkg/.gitignore | 2 + docs/pkg/cherrypy.lib.rst | 142 ------------ docs/pkg/cherrypy.process.rst | 46 ---- docs/pkg/cherrypy.rst | 33 --- docs/pkg/cherrypy.scaffold.rst | 10 - docs/pkg/cherrypy.test.rst | 390 --------------------------------- docs/pkg/cherrypy.tutorial.rst | 94 -------- docs/pkg/modules.rst | 7 - setup.py | 1 + 11 files changed, 27 insertions(+), 723 deletions(-) create mode 100644 docs/pkg/.gitignore delete mode 100644 docs/pkg/cherrypy.lib.rst delete mode 100644 docs/pkg/cherrypy.process.rst delete mode 100644 docs/pkg/cherrypy.rst delete mode 100644 docs/pkg/cherrypy.scaffold.rst delete mode 100644 docs/pkg/cherrypy.test.rst delete mode 100644 docs/pkg/cherrypy.tutorial.rst delete mode 100644 docs/pkg/modules.rst diff --git a/docs/conf.py b/docs/conf.py index c277b6e48..111293191 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -80,12 +80,16 @@ def get_supported_pythons(classifiers): # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ + # Stdlib extensions: 'sphinx.ext.autodoc', 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', + + # Third-party extensions: + 'sphinxcontrib.apidoc', 'rst.linker', 'jaraco.packaging.sphinx', ] @@ -238,3 +242,17 @@ def mock_pywin32(): # Ref: https://github.com/python-attrs/attrs/pull/571/files\ # #diff-85987f48f1258d9ee486e3191495582dR82 default_role = 'any' + + +# -- Options for apidoc extension ---------------------------------------- + +apidoc_excluded_paths = [] +apidoc_extra_args = [ + '--implicit-namespaces', + '--private', # include “_private” modules +] +apidoc_module_dir = '../cherrypy' +apidoc_module_first = False +apidoc_output_dir = 'pkg' +apidoc_separate_modules = True +apidoc_toc_file = None diff --git a/docs/index.rst b/docs/index.rst index bbd0fe6dd..a2e35029b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,7 +19,12 @@ CherryPy — A Minimalist Python Web Framework development.rst glossary.rst history.rst - pkg/modules.rst + +.. toctree:: + :hidden: + :caption: Reference + + pkg/modules `CherryPy `_ is a pythonic, object-oriented web framework. diff --git a/docs/pkg/.gitignore b/docs/pkg/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/docs/pkg/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/docs/pkg/cherrypy.lib.rst b/docs/pkg/cherrypy.lib.rst deleted file mode 100644 index 8df1261c3..000000000 --- a/docs/pkg/cherrypy.lib.rst +++ /dev/null @@ -1,142 +0,0 @@ -cherrypy.lib package -==================== - -Submodules ----------- - -cherrypy.lib.auth_basic module ------------------------------- - -.. automodule:: cherrypy.lib.auth_basic - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.auth_digest module -------------------------------- - -.. automodule:: cherrypy.lib.auth_digest - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.caching module ---------------------------- - -.. automodule:: cherrypy.lib.caching - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.covercp module ---------------------------- - -.. automodule:: cherrypy.lib.covercp - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.cpstats module ---------------------------- - -.. automodule:: cherrypy.lib.cpstats - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.cptools module ---------------------------- - -.. automodule:: cherrypy.lib.cptools - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.encoding module ----------------------------- - -.. automodule:: cherrypy.lib.encoding - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.gctools module ---------------------------- - -.. automodule:: cherrypy.lib.gctools - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.httputil module ----------------------------- - -.. automodule:: cherrypy.lib.httputil - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.jsontools module ------------------------------ - -.. automodule:: cherrypy.lib.jsontools - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.locking module ---------------------------- - -.. automodule:: cherrypy.lib.locking - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.profiler module ----------------------------- - -.. automodule:: cherrypy.lib.profiler - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.reprconf module ----------------------------- - -.. automodule:: cherrypy.lib.reprconf - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.sessions module ----------------------------- - -.. automodule:: cherrypy.lib.sessions - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.static module --------------------------- - -.. automodule:: cherrypy.lib.static - :members: - :undoc-members: - :show-inheritance: - -cherrypy.lib.xmlrpcutil module ------------------------------- - -.. automodule:: cherrypy.lib.xmlrpcutil - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: cherrypy.lib - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/pkg/cherrypy.process.rst b/docs/pkg/cherrypy.process.rst deleted file mode 100644 index 86fa54363..000000000 --- a/docs/pkg/cherrypy.process.rst +++ /dev/null @@ -1,46 +0,0 @@ -cherrypy.process package -======================== - -Submodules ----------- - -cherrypy.process.plugins module -------------------------------- - -.. automodule:: cherrypy.process.plugins - :members: - :undoc-members: - :show-inheritance: - -cherrypy.process.servers module -------------------------------- - -.. automodule:: cherrypy.process.servers - :members: - :undoc-members: - :show-inheritance: - -cherrypy.process.win32 module ------------------------------ - -.. automodule:: cherrypy.process.win32 - :members: - :undoc-members: - :show-inheritance: - -cherrypy.process.wspbus module ------------------------------- - -.. automodule:: cherrypy.process.wspbus - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: cherrypy.process - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/pkg/cherrypy.rst b/docs/pkg/cherrypy.rst deleted file mode 100644 index 38599fa12..000000000 --- a/docs/pkg/cherrypy.rst +++ /dev/null @@ -1,33 +0,0 @@ -cherrypy package -================ - -Subpackages ------------ - -.. toctree:: - - cherrypy.lib - cherrypy.process - cherrypy.scaffold - cherrypy.test - cherrypy.tutorial - -Submodules ----------- - -cherrypy.daemon module ----------------------- - -.. automodule:: cherrypy.daemon - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: cherrypy - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/pkg/cherrypy.scaffold.rst b/docs/pkg/cherrypy.scaffold.rst deleted file mode 100644 index c13adbf0d..000000000 --- a/docs/pkg/cherrypy.scaffold.rst +++ /dev/null @@ -1,10 +0,0 @@ -cherrypy.scaffold package -========================= - -Module contents ---------------- - -.. automodule:: cherrypy.scaffold - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/pkg/cherrypy.test.rst b/docs/pkg/cherrypy.test.rst deleted file mode 100644 index de05e2923..000000000 --- a/docs/pkg/cherrypy.test.rst +++ /dev/null @@ -1,390 +0,0 @@ -cherrypy.test package -===================== - -Submodules ----------- - -cherrypy.test.benchmark module ------------------------------- - -.. automodule:: cherrypy.test.benchmark - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.checkerdemo module --------------------------------- - -.. automodule:: cherrypy.test.checkerdemo - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.helper module ---------------------------- - -.. automodule:: cherrypy.test.helper - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.logtest module ----------------------------- - -.. automodule:: cherrypy.test.logtest - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.modfastcgi module -------------------------------- - -.. automodule:: cherrypy.test.modfastcgi - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.modfcgid module ------------------------------ - -.. automodule:: cherrypy.test.modfcgid - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.modpy module --------------------------- - -.. automodule:: cherrypy.test.modpy - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.modwsgi module ----------------------------- - -.. automodule:: cherrypy.test.modwsgi - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.sessiondemo module --------------------------------- - -.. automodule:: cherrypy.test.sessiondemo - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_auth_basic module ------------------------------------- - -.. automodule:: cherrypy.test.test_auth_basic - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_auth_digest module -------------------------------------- - -.. automodule:: cherrypy.test.test_auth_digest - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_bus module ------------------------------ - -.. automodule:: cherrypy.test.test_bus - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_caching module ---------------------------------- - -.. automodule:: cherrypy.test.test_caching - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_compat module --------------------------------- - -.. automodule:: cherrypy.test.test_compat - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_config module --------------------------------- - -.. automodule:: cherrypy.test.test_config - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_config_server module ---------------------------------------- - -.. automodule:: cherrypy.test.test_config_server - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_conn module ------------------------------- - -.. automodule:: cherrypy.test.test_conn - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_core module ------------------------------- - -.. automodule:: cherrypy.test.test_core - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_dynamicobjectmapping module ----------------------------------------------- - -.. automodule:: cherrypy.test.test_dynamicobjectmapping - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_encoding module ----------------------------------- - -.. automodule:: cherrypy.test.test_encoding - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_etags module -------------------------------- - -.. automodule:: cherrypy.test.test_etags - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_http module ------------------------------- - -.. automodule:: cherrypy.test.test_http - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_httplib module ---------------------------------- - -.. automodule:: cherrypy.test.test_httplib - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_iterator module ----------------------------------- - -.. automodule:: cherrypy.test.test_iterator - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_json module ------------------------------- - -.. automodule:: cherrypy.test.test_json - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_logging module ---------------------------------- - -.. automodule:: cherrypy.test.test_logging - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_mime module ------------------------------- - -.. automodule:: cherrypy.test.test_mime - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_misc_tools module ------------------------------------- - -.. automodule:: cherrypy.test.test_misc_tools - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_objectmapping module ---------------------------------------- - -.. automodule:: cherrypy.test.test_objectmapping - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_params module --------------------------------- - -.. automodule:: cherrypy.test.test_params - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_proxy module -------------------------------- - -.. automodule:: cherrypy.test.test_proxy - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_refleaks module ----------------------------------- - -.. automodule:: cherrypy.test.test_refleaks - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_request_obj module -------------------------------------- - -.. automodule:: cherrypy.test.test_request_obj - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_routes module --------------------------------- - -.. automodule:: cherrypy.test.test_routes - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_session module ---------------------------------- - -.. automodule:: cherrypy.test.test_session - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_sessionauthenticate module ---------------------------------------------- - -.. automodule:: cherrypy.test.test_sessionauthenticate - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_states module --------------------------------- - -.. automodule:: cherrypy.test.test_states - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_static module --------------------------------- - -.. automodule:: cherrypy.test.test_static - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_tools module -------------------------------- - -.. automodule:: cherrypy.test.test_tools - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_tutorials module ------------------------------------ - -.. automodule:: cherrypy.test.test_tutorials - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_virtualhost module -------------------------------------- - -.. automodule:: cherrypy.test.test_virtualhost - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_wsgi_ns module ---------------------------------- - -.. automodule:: cherrypy.test.test_wsgi_ns - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_wsgi_unix_socket module ------------------------------------------- - -.. automodule:: cherrypy.test.test_wsgi_unix_socket - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_wsgi_vhost module ------------------------------------- - -.. automodule:: cherrypy.test.test_wsgi_vhost - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_wsgiapps module ----------------------------------- - -.. automodule:: cherrypy.test.test_wsgiapps - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.test_xmlrpc module --------------------------------- - -.. automodule:: cherrypy.test.test_xmlrpc - :members: - :undoc-members: - :show-inheritance: - -cherrypy.test.webtest module ----------------------------- - -.. automodule:: cherrypy.test.webtest - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: cherrypy.test - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/pkg/cherrypy.tutorial.rst b/docs/pkg/cherrypy.tutorial.rst deleted file mode 100644 index 3676e0ba2..000000000 --- a/docs/pkg/cherrypy.tutorial.rst +++ /dev/null @@ -1,94 +0,0 @@ -cherrypy.tutorial package -========================= - -Submodules ----------- - -cherrypy.tutorial.tut01_helloworld module ------------------------------------------ - -.. automodule:: cherrypy.tutorial.tut01_helloworld - :members: - :undoc-members: - :show-inheritance: - -cherrypy.tutorial.tut02_expose_methods module ---------------------------------------------- - -.. automodule:: cherrypy.tutorial.tut02_expose_methods - :members: - :undoc-members: - :show-inheritance: - -cherrypy.tutorial.tut03_get_and_post module -------------------------------------------- - -.. automodule:: cherrypy.tutorial.tut03_get_and_post - :members: - :undoc-members: - :show-inheritance: - -cherrypy.tutorial.tut04_complex_site module -------------------------------------------- - -.. automodule:: cherrypy.tutorial.tut04_complex_site - :members: - :undoc-members: - :show-inheritance: - -cherrypy.tutorial.tut05_derived_objects module ----------------------------------------------- - -.. automodule:: cherrypy.tutorial.tut05_derived_objects - :members: - :undoc-members: - :show-inheritance: - -cherrypy.tutorial.tut06_default_method module ---------------------------------------------- - -.. automodule:: cherrypy.tutorial.tut06_default_method - :members: - :undoc-members: - :show-inheritance: - -cherrypy.tutorial.tut07_sessions module ---------------------------------------- - -.. automodule:: cherrypy.tutorial.tut07_sessions - :members: - :undoc-members: - :show-inheritance: - -cherrypy.tutorial.tut08_generators_and_yield module ---------------------------------------------------- - -.. automodule:: cherrypy.tutorial.tut08_generators_and_yield - :members: - :undoc-members: - :show-inheritance: - -cherrypy.tutorial.tut09_files module ------------------------------------- - -.. automodule:: cherrypy.tutorial.tut09_files - :members: - :undoc-members: - :show-inheritance: - -cherrypy.tutorial.tut10_http_errors module ------------------------------------------- - -.. automodule:: cherrypy.tutorial.tut10_http_errors - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: cherrypy.tutorial - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/pkg/modules.rst b/docs/pkg/modules.rst deleted file mode 100644 index 9c68b63a0..000000000 --- a/docs/pkg/modules.rst +++ /dev/null @@ -1,7 +0,0 @@ -Modules -======= - -.. toctree:: - :maxdepth: 4 - - cherrypy diff --git a/setup.py b/setup.py index 5c715e89f..2483bb3be 100644 --- a/setup.py +++ b/setup.py @@ -73,6 +73,7 @@ 'sphinx', 'docutils', 'alabaster', + 'sphinxcontrib-apidoc>=0.3.0', 'rst.linker>=1.11', 'jaraco.packaging>=3.2', ], From 669dc9a6fc72bbd8b011277b66a8298745f8e4e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hrn=C4=8Diar?= Date: Mon, 3 May 2021 14:00:33 +0200 Subject: [PATCH 005/322] Require setuptools, docs/conf.py, cherrypy/test/helper.py import pkg_resources --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 2483bb3be..162650512 100644 --- a/setup.py +++ b/setup.py @@ -76,6 +76,7 @@ 'sphinxcontrib-apidoc>=0.3.0', 'rst.linker>=1.11', 'jaraco.packaging>=3.2', + 'setuptools', ], 'json': ['simplejson'], 'routes_dispatcher': ['routes>=2.3.1'], @@ -94,6 +95,7 @@ 'path.py', 'requests_toolbelt', 'pytest-services>=2', + 'setuptools', ], # Enables memcached session support via `cherrypy[memcached_session]`: 'memcached_session': ['python-memcached>=1.58'], From 9e54994a178f688e3b1cb00bdd4658d1d7bce743 Mon Sep 17 00:00:00 2001 From: Dominic Davis-Foster Date: Mon, 7 Jun 2021 16:53:21 +0100 Subject: [PATCH 006/322] Don't use deprecated camelCase functions from threading module. --- cherrypy/process/plugins.py | 10 +++++----- cherrypy/process/servers.py | 2 +- cherrypy/process/wspbus.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cherrypy/process/plugins.py b/cherrypy/process/plugins.py index d2f87a4d4..2a9952de1 100644 --- a/cherrypy/process/plugins.py +++ b/cherrypy/process/plugins.py @@ -366,7 +366,7 @@ def start(self): # "The general problem with making fork() work in a multi-threaded # world is what to do with all of the threads..." # So we check for active threads: - if threading.activeCount() != 1: + if threading.active_count() != 1: self.bus.log('There are %r active threads. ' 'Daemonizing now may cause strange failures.' % threading.enumerate(), level=30) @@ -552,7 +552,7 @@ def start(self): if self.thread is None: self.thread = BackgroundTask(self.frequency, self.callback, bus=self.bus) - self.thread.setName(threadname) + self.thread.name = threadname self.thread.start() self.bus.log('Started monitor thread %r.' % threadname) else: @@ -565,8 +565,8 @@ def stop(self): self.bus.log('No thread running for %s.' % self.name or self.__class__.__name__) else: - if self.thread is not threading.currentThread(): - name = self.thread.getName() + if self.thread is not threading.current_thread(): + name = self.thread.name self.thread.cancel() if not self.thread.daemon: self.bus.log('Joining %r' % name) @@ -692,7 +692,7 @@ def run(self): filename) self.thread.cancel() self.bus.log('Stopped thread %r.' % - self.thread.getName()) + self.thread.name) self.bus.restart() return diff --git a/cherrypy/process/servers.py b/cherrypy/process/servers.py index dcb34de61..717a8de0f 100644 --- a/cherrypy/process/servers.py +++ b/cherrypy/process/servers.py @@ -178,7 +178,7 @@ def start(self): import threading t = threading.Thread(target=self._start_http_thread) - t.setName('HTTPServer ' + t.getName()) + t.name = 'HTTPServer ' + t.name t.start() self.wait() diff --git a/cherrypy/process/wspbus.py b/cherrypy/process/wspbus.py index ead90a4e2..1d2789b1f 100644 --- a/cherrypy/process/wspbus.py +++ b/cherrypy/process/wspbus.py @@ -356,13 +356,13 @@ def block(self, interval=0.1): # implemented as a windows service and in any other case # that another thread executes cherrypy.engine.exit() if ( - t != threading.currentThread() and + t != threading.current_thread() and not isinstance(t, threading._MainThread) and # Note that any dummy (external) threads are # always daemonic. not t.daemon ): - self.log('Waiting for thread %s.' % t.getName()) + self.log('Waiting for thread %s.' % t.name) t.join() if self.execv: @@ -570,7 +570,7 @@ def _callback(func, *a, **kw): self.wait(states.STARTED) func(*a, **kw) t = threading.Thread(target=_callback, args=args, kwargs=kwargs) - t.setName('Bus Callback ' + t.getName()) + t.name = 'Bus Callback ' + t.name t.start() self.start() From cccee6b1b816f2ac87085abbede353559dd2228c Mon Sep 17 00:00:00 2001 From: Priyansh Singh <63330165+ps-19@users.noreply.github.com> Date: Thu, 17 Jun 2021 12:42:28 +0530 Subject: [PATCH 007/322] Updated README.rst --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 75a2f5323..71de04d4c 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,7 @@ CherryPy is a pythonic, object-oriented HTTP framework. 1. It allows building web applications in much the same way one would build any other object-oriented program. -2. This design results in less and more readable code being developed faster. +2. This design results in short and more readable code being developed faster. It's all just properties and methods. 3. It is now more than ten years old and has proven fast and very stable. @@ -79,7 +79,7 @@ Here's how easy it is to write "Hello World" in CherryPy: cherrypy.quickstart(HelloWorld()) And it continues to work that intuitively when systems grow, allowing -for the Python object model to be dynamically presented as a web site +for the Python object model to be dynamically presented as a website and/or API. While CherryPy is one of the easiest and most intuitive frameworks out From 3bd15de54e327826f49f52575bd1e57222b5714f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 17 Jun 2021 09:40:42 -0400 Subject: [PATCH 008/322] Use 'concise' for more precise and unambiguous phrasing. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 71de04d4c..d579a57b6 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,7 @@ CherryPy is a pythonic, object-oriented HTTP framework. 1. It allows building web applications in much the same way one would build any other object-oriented program. -2. This design results in short and more readable code being developed faster. +2. This design results in more concise and readable code developed faster. It's all just properties and methods. 3. It is now more than ten years old and has proven fast and very stable. From c36c8086930c3201c93f419b37d76af3298f3734 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 Jul 2021 18:23:01 -0400 Subject: [PATCH 009/322] Set a minimum version of pywin32 to protect pip from an excruciating trial. Ref #1920. --- CHANGES.rst | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index d78475b46..793dbabe7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,8 @@ v18.6.1 CPython so that it won't get pulled-in under PyPy -- by :user:`webknjaz`. +* :issue:`1920`: Bumped minimum version of PyWin32 to 227. + v18.6.0 ------- diff --git a/setup.py b/setup.py index 162650512..d1fb7c38a 100644 --- a/setup.py +++ b/setup.py @@ -103,7 +103,7 @@ # https://docs.cherrypy.org/en/latest/advanced.html?highlight=windows#windows-console-events ':sys_platform == "win32" and implementation_name == "cpython"': [ - 'pywin32', + 'pywin32 >= 227', ], }, setup_requires=[ From 2aeb089e929c670721e57010662c5023c043736e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 Jul 2021 18:30:08 -0400 Subject: [PATCH 010/322] Block installation of PyWin32 on Python 3.10 until a release can be made. Fixes #1920. --- CHANGES.rst | 1 + setup.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 793dbabe7..70913d591 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,7 @@ v18.6.1 -- by :user:`webknjaz`. * :issue:`1920`: Bumped minimum version of PyWin32 to 227. + Block pywin32 install on Python 3.10 and later. v18.6.0 ------- diff --git a/setup.py b/setup.py index d1fb7c38a..27b16c08d 100644 --- a/setup.py +++ b/setup.py @@ -102,7 +102,9 @@ 'xcgi': ['flup'], # https://docs.cherrypy.org/en/latest/advanced.html?highlight=windows#windows-console-events - ':sys_platform == "win32" and implementation_name == "cpython"': [ + ':sys_platform == "win32" and implementation_name == "cpython"' + # pywin32 disabled while a build is unavailable. Ref #1920. + ' and python_version < "3.10"': [ 'pywin32 >= 227', ], }, From 20155984889311542ba1bb8c06b52529452c7a06 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 Jul 2021 19:24:17 -0400 Subject: [PATCH 011/322] Drop support for Python 3.5 --- .appveyor.yml | 1 - .circleci/config.yml | 8 ++++---- .travis.yml | 12 ------------ CHANGES.rst | 5 +++++ setup.py | 3 +-- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index ba6c980bb..08d7ba84e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,7 +2,6 @@ environment: matrix: - PYTHON: "C:\\Python37-x64" - PYTHON: "C:\\Python36-x64" - - PYTHON: "C:\\Python35-x64" init: - "chcp 65001" diff --git a/.circleci/config.yml b/.circleci/config.yml index 9a3a8b380..4f6da680a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,17 +15,17 @@ jobs: ' >> $BASH_ENV - run: |- - for py_ver in 3.7.0 3.6.4 3.5.4 pypy3.5-6.0.0 + for py_ver in 3.7.0 3.6.4 do pyenv install "$py_ver" & done wait - - run: pyenv global 3.7.0 3.6.4 3.5.4 pypy3.5-6.0.0 + - run: pyenv global 3.7.0 3.6.4 - run: python3 -m pip install --upgrade pip wheel - run: python3 -m pip install tox tox-pyenv - checkout - - run: tox -e py35,py36,py37 -- -p no:sugar # , pypy3 + - run: tox -e py36,py37 -- -p no:sugar - store_test_results: path: .test-results - store_artifacts: @@ -38,7 +38,7 @@ jobs: steps: - checkout - run: pip install tox - - run: tox -e py35,py36,py37 + - run: tox -e py36,py37 - store_test_results: path: .test-results - store_artifacts: diff --git a/.travis.yml b/.travis.yml index c8d50b1f7..b0b0c60d1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -73,15 +73,9 @@ _base_envs: python: 3.9 after_failure: skip python: -- 3.5 - 3.7-dev -- &pypy3 pypy3.5-5.10.0 jobs: fast_finish: true - allow_failures: - # TODO: fix tests - - python: *pypy3 - - env: TOXENV=pre-commit-pep257 include: - <<: *lint_python_base env: TOXENV=pre-commit @@ -104,12 +98,6 @@ jobs: env: TOXENV=cheroot-master - <<: *pure_python_base_priority python: nightly - - <<: *osx_python_base - python: 3.5 - env: - - PYTHON_VERSION=3.5.5 - - *env_pyenv - - *env_path - <<: *osx_python_base python: *mainstream_python env: diff --git a/CHANGES.rst b/CHANGES.rst index 70913d591..9e081b5af 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v18.7.0 +------- + +* :pr:`1923`: Drop support for Python 3.5. + v18.6.1 ------- diff --git a/setup.py b/setup.py index 27b16c08d..c617526ca 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,6 @@ 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', @@ -111,7 +110,7 @@ setup_requires=[ 'setuptools_scm', ], - python_requires='>=3.5', + python_requires='>=3.6', ) From a8873641e295aed2ea301b7744c29d1e1105c8b7 Mon Sep 17 00:00:00 2001 From: Maneesh Babu M Date: Wed, 1 Sep 2021 16:24:40 +0530 Subject: [PATCH 012/322] Fix broken links --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index d579a57b6..eb6500f0e 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,7 @@ :target: https://codecov.io/gh/cherrypy/cherrypy :alt: codecov -Welcome to the GitHub repository of `CherryPy `_! +Welcome to the GitHub repository of `CherryPy `_! CherryPy is a pythonic, object-oriented HTTP framework. @@ -84,7 +84,7 @@ and/or API. While CherryPy is one of the easiest and most intuitive frameworks out there, the prerequisite for understanding the `CherryPy -documentation `_ is that you have +documentation `_ is that you have a general understanding of Python and web development. Additionally: @@ -95,7 +95,7 @@ Additionally: If the docs are insufficient to address your needs, the CherryPy community has several `avenues for support -`_. +`_. For Enterprise -------------- @@ -112,6 +112,6 @@ Contributing ------------ Please follow the `contribution guidelines -`_. +`_. And by all means, absorb the `Zen of CherryPy `_. From 123152e7e38756af6195f069f432e1eeb832db94 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 7 Sep 2021 20:09:18 +0200 Subject: [PATCH 013/322] Use cherrypy.dev in links --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index eb6500f0e..9f507911f 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,7 @@ :target: https://codecov.io/gh/cherrypy/cherrypy :alt: codecov -Welcome to the GitHub repository of `CherryPy `_! +Welcome to the GitHub repository of `CherryPy `_! CherryPy is a pythonic, object-oriented HTTP framework. @@ -84,7 +84,7 @@ and/or API. While CherryPy is one of the easiest and most intuitive frameworks out there, the prerequisite for understanding the `CherryPy -documentation `_ is that you have +documentation `_ is that you have a general understanding of Python and web development. Additionally: @@ -95,7 +95,7 @@ Additionally: If the docs are insufficient to address your needs, the CherryPy community has several `avenues for support -`_. +`_. For Enterprise -------------- @@ -112,6 +112,6 @@ Contributing ------------ Please follow the `contribution guidelines -`_. +`_. And by all means, absorb the `Zen of CherryPy `_. From 07aec3c90326e537613e5a77584df201ac5fdb1a Mon Sep 17 00:00:00 2001 From: Maneesh Babu M Date: Sat, 11 Sep 2021 12:14:20 +0000 Subject: [PATCH 014/322] Replace cherrypy.org to cherrypy.dev --- .github/CONTRIBUTING.rst | 2 +- .github/SUPPORT.rst | 2 +- LICENSE.md | 2 +- README.rst | 2 +- cherrypy/_cperror.py | 2 +- cherrypy/_cprequest.py | 4 ++-- cherrypy/test/modfastcgi.py | 2 +- cherrypy/test/modfcgid.py | 2 +- cherrypy/test/modpy.py | 2 +- cherrypy/test/modwsgi.py | 2 +- cherrypy/test/test_auth_basic.py | 2 +- cherrypy/test/test_auth_digest.py | 2 +- cherrypy/test/test_encoding.py | 2 +- cherrypy/test/test_logging.py | 4 ++-- cherrypy/test/test_request_obj.py | 6 +++--- cherrypy/test/test_tutorials.py | 2 +- cherrypy/tutorial/tut04_complex_site.py | 4 ++-- docs/conf.py | 2 +- docs/contribute.rst | 2 +- docs/index.rst | 2 +- man/cherryd.1 | 2 +- setup.py | 8 ++++---- 22 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/CONTRIBUTING.rst b/.github/CONTRIBUTING.rst index dd21f7538..999f1af68 100644 --- a/.github/CONTRIBUTING.rst +++ b/.github/CONTRIBUTING.rst @@ -28,5 +28,5 @@ python version, and any other related software versions. Also ---- -See `Contributing `_ in +See `Contributing `_ in the docs. diff --git a/.github/SUPPORT.rst b/.github/SUPPORT.rst index 1c31c2422..618792f57 100644 --- a/.github/SUPPORT.rst +++ b/.github/SUPPORT.rst @@ -9,7 +9,7 @@ I have a question ----------------- If you have a question and cannot find an answer for it in issues or the -the `documentation `__, `please +the `documentation `__, `please create an issue `__. Questions and their answers have great value for the community, and a diff --git a/LICENSE.md b/LICENSE.md index 96b866459..ce28cf8f2 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright © 2004-2019, CherryPy Team (team@cherrypy.org) +Copyright © 2004-2019, CherryPy Team (team@cherrypy.dev) All rights reserved. diff --git a/README.rst b/README.rst index 9f507911f..d7b4b2474 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,7 @@ .. image:: https://readthedocs.org/projects/cherrypy/badge/?version=latest - :target: https://docs.cherrypy.org/en/latest/?badge=latest + :target: https://docs.cherrypy.dev/en/latest/?badge=latest .. image:: https://img.shields.io/badge/StackOverflow-CherryPy-blue.svg :target: https://stackoverflow.com/questions/tagged/cheroot+or+cherrypy diff --git a/cherrypy/_cperror.py b/cherrypy/_cperror.py index 4e7276827..31640335d 100644 --- a/cherrypy/_cperror.py +++ b/cherrypy/_cperror.py @@ -466,7 +466,7 @@ def __init__(self, path=None):
%(traceback)s
- Powered by CherryPy %(version)s + Powered by CherryPy %(version)s
diff --git a/cherrypy/_cprequest.py b/cherrypy/_cprequest.py index 9b86bd674..b380bb75a 100644 --- a/cherrypy/_cprequest.py +++ b/cherrypy/_cprequest.py @@ -169,7 +169,7 @@ def request_namespace(k, v): def response_namespace(k, v): """Attach response attributes declared in config.""" # Provides config entries to set default response headers - # http://cherrypy.org/ticket/889 + # http://cherrypy.dev/ticket/889 if k[:8] == 'headers.': cherrypy.serving.response.headers[k.split('.', 1)[1]] = v else: @@ -252,7 +252,7 @@ class Request(object): The query component of the Request-URI, a string of information to be interpreted by the resource. The query portion of a URI follows the path component, and is separated by a '?'. For example, the URI - 'http://www.cherrypy.org/wiki?a=3&b=4' has the query component, + 'http://www.cherrypy.dev/wiki?a=3&b=4' has the query component, 'a=3&b=4'.""" query_string_encoding = 'utf8' diff --git a/cherrypy/test/modfastcgi.py b/cherrypy/test/modfastcgi.py index 79ec3d182..3c34a7f92 100644 --- a/cherrypy/test/modfastcgi.py +++ b/cherrypy/test/modfastcgi.py @@ -14,7 +14,7 @@ 1. Apache processes Range headers automatically; CherryPy's truncated output is then truncated again by Apache. See test_core.testRanges. - This was worked around in http://www.cherrypy.org/changeset/1319. + This was worked around in http://www.cherrypy.dev/changeset/1319. 2. Apache does not allow custom HTTP methods like CONNECT as per the spec. See test_core.testHTTPMethods. 3. Max request header and body settings do not work with Apache. diff --git a/cherrypy/test/modfcgid.py b/cherrypy/test/modfcgid.py index d101bd67f..b2a0c08cc 100644 --- a/cherrypy/test/modfcgid.py +++ b/cherrypy/test/modfcgid.py @@ -14,7 +14,7 @@ 1. Apache processes Range headers automatically; CherryPy's truncated output is then truncated again by Apache. See test_core.testRanges. - This was worked around in http://www.cherrypy.org/changeset/1319. + This was worked around in http://www.cherrypy.dev/changeset/1319. 2. Apache does not allow custom HTTP methods like CONNECT as per the spec. See test_core.testHTTPMethods. 3. Max request header and body settings do not work with Apache. diff --git a/cherrypy/test/modpy.py b/cherrypy/test/modpy.py index 7c288d2c0..04117ff39 100644 --- a/cherrypy/test/modpy.py +++ b/cherrypy/test/modpy.py @@ -15,7 +15,7 @@ 1. Apache processes Range headers automatically; CherryPy's truncated output is then truncated again by Apache. See test_core.testRanges. - This was worked around in http://www.cherrypy.org/changeset/1319. + This was worked around in http://www.cherrypy.dev/changeset/1319. 2. Apache does not allow custom HTTP methods like CONNECT as per the spec. See test_core.testHTTPMethods. 3. Max request header and body settings do not work with Apache. diff --git a/cherrypy/test/modwsgi.py b/cherrypy/test/modwsgi.py index da7d240b5..bb4340cc3 100644 --- a/cherrypy/test/modwsgi.py +++ b/cherrypy/test/modwsgi.py @@ -11,7 +11,7 @@ 1. Apache processes Range headers automatically; CherryPy's truncated output is then truncated again by Apache. See test_core.testRanges. - This was worked around in http://www.cherrypy.org/changeset/1319. + This was worked around in http://www.cherrypy.dev/changeset/1319. 2. Apache does not allow custom HTTP methods like CONNECT as per the spec. See test_core.testHTTPMethods. 3. Max request header and body settings do not work with Apache. diff --git a/cherrypy/test/test_auth_basic.py b/cherrypy/test/test_auth_basic.py index d7e69a9b4..f178f8f97 100644 --- a/cherrypy/test/test_auth_basic.py +++ b/cherrypy/test/test_auth_basic.py @@ -1,4 +1,4 @@ -# This file is part of CherryPy +# This file is part of CherryPy # -*- coding: utf-8 -*- # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 diff --git a/cherrypy/test/test_auth_digest.py b/cherrypy/test/test_auth_digest.py index 745f89e6c..4b7b5298f 100644 --- a/cherrypy/test/test_auth_digest.py +++ b/cherrypy/test/test_auth_digest.py @@ -1,4 +1,4 @@ -# This file is part of CherryPy +# This file is part of CherryPy # -*- coding: utf-8 -*- # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 diff --git a/cherrypy/test/test_encoding.py b/cherrypy/test/test_encoding.py index 882d7a5b8..6075103d8 100644 --- a/cherrypy/test/test_encoding.py +++ b/cherrypy/test/test_encoding.py @@ -46,7 +46,7 @@ def cookies_and_headers(self): # any part which is unicode (even ascii), the response # should not fail. cherrypy.response.cookie['candy'] = 'bar' - cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org' + cherrypy.response.cookie['candy']['domain'] = 'cherrypy.dev' cherrypy.response.headers[ 'Some-Header'] = 'My d\xc3\xb6g has fleas' cherrypy.response.headers[ diff --git a/cherrypy/test/test_logging.py b/cherrypy/test/test_logging.py index 5308fb72f..2d4aa56fd 100644 --- a/cherrypy/test/test_logging.py +++ b/cherrypy/test/test_logging.py @@ -113,7 +113,7 @@ def test_normal_return(log_tracker, server): resp = requests.get( 'http://%s:%s/as_string' % (host, port), headers={ - 'Referer': 'http://www.cherrypy.org/', + 'Referer': 'http://www.cherrypy.dev/', 'User-Agent': 'Mozilla/5.0', }, ) @@ -135,7 +135,7 @@ def test_normal_return(log_tracker, server): log_tracker.assertLog( -1, '] "GET /as_string HTTP/1.1" 200 %s ' - '"http://www.cherrypy.org/" "Mozilla/5.0"' + '"http://www.cherrypy.dev/" "Mozilla/5.0"' % content_length, ) diff --git a/cherrypy/test/test_request_obj.py b/cherrypy/test/test_request_obj.py index 31023e8fc..3aaa8e817 100644 --- a/cherrypy/test/test_request_obj.py +++ b/cherrypy/test/test_request_obj.py @@ -342,7 +342,7 @@ def testRelativeURIPathInfo(self): self.assertBody('/pathinfo/foo/bar') def testAbsoluteURIPathInfo(self): - # http://cherrypy.org/ticket/1061 + # http://cherrypy.dev/ticket/1061 self.getPage('http://localhost/pathinfo/foo/bar') self.assertBody('/pathinfo/foo/bar') @@ -375,10 +375,10 @@ def testParams(self): # Make sure that encoded = and & get parsed correctly self.getPage( - '/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2') + '/params/code?url=http%3A//cherrypy.dev/index%3Fa%3D1%26b%3D2') self.assertBody('args: %s kwargs: %s' % (('code',), - [('url', ntou('http://cherrypy.org/index?a=1&b=2'))])) + [('url', ntou('http://cherrypy.dev/index?a=1&b=2'))])) # Test coordinates sent by self.getPage('/params/ismap?223,114') diff --git a/cherrypy/test/test_tutorials.py b/cherrypy/test/test_tutorials.py index 39ca4d6f2..d4835a90c 100644 --- a/cherrypy/test/test_tutorials.py +++ b/cherrypy/test/test_tutorials.py @@ -78,7 +78,7 @@ def test04ComplexSite(self):

[Return to links page]

''' diff --git a/cherrypy/tutorial/tut04_complex_site.py b/cherrypy/tutorial/tut04_complex_site.py index 3caa1775d..fe04054f1 100644 --- a/cherrypy/tutorial/tut04_complex_site.py +++ b/cherrypy/tutorial/tut04_complex_site.py @@ -53,7 +53,7 @@ def index(self):
  • - The CherryPy Homepage + The CherryPy Homepage
  • The Python Homepage @@ -77,7 +77,7 @@ def index(self):

    [Return to links page]

    ''' diff --git a/docs/conf.py b/docs/conf.py index 111293191..480edef47 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -106,7 +106,7 @@ def get_supported_pythons(classifiers): intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), - 'cheroot': ('https://cheroot.cherrypy.org/en/latest/', None), + 'cheroot': ('https://cheroot.cherrypy.dev/en/latest/', None), 'pytest-docs': ('https://docs.pytest.org/en/latest/', None), } diff --git a/docs/contribute.rst b/docs/contribute.rst index f00552938..f4ba3374d 100644 --- a/docs/contribute.rst +++ b/docs/contribute.rst @@ -6,7 +6,7 @@ The project actively encourages aspiring and experienced users to dive in and add their best contribution to the project. How can you contribute? Well, first search the `docs -`_ and the `project page +`_ and the `project page `_ to see if someone has already reported your issue. diff --git a/docs/index.rst b/docs/index.rst index a2e35029b..6914a8dde 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,7 +26,7 @@ CherryPy — A Minimalist Python Web Framework pkg/modules -`CherryPy `_ is a pythonic, object-oriented web framework. +`CherryPy `_ is a pythonic, object-oriented web framework. CherryPy allows developers to build web applications in much the same way they would build any other object-oriented Python program. diff --git a/man/cherryd.1 b/man/cherryd.1 index 1811660a1..4e838bbea 100644 --- a/man/cherryd.1 +++ b/man/cherryd.1 @@ -253,7 +253,7 @@ These are the built\-in environment configurations: fumanchu .nf -cherrypy.org +cherrypy.dev .fi .SH COPYRIGHT diff --git a/setup.py b/setup.py index c617526ca..bd1e38f1c 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ use_scm_version=True, description='Object-Oriented HTTP framework', author='CherryPy Team', - author_email='team@cherrypy.org', + author_email='team@cherrypy.dev', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -40,12 +40,12 @@ 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', 'Topic :: Software Development :: Libraries :: Application Frameworks', ], - url='https://www.cherrypy.org', + url='https://www.cherrypy.dev', project_urls={ 'CI: AppVeyor': 'https://ci.appveyor.com/project/{}'.format(repo_slug), 'CI: Travis': 'https://travis-ci.org/{}'.format(repo_slug), 'CI: Circle': 'https://circleci.com/gh/{}'.format(repo_slug), - 'Docs: RTD': 'https://docs.cherrypy.org', + 'Docs: RTD': 'https://docs.cherrypy.dev', 'GitHub: issues': '{}/issues'.format(repo_url), 'GitHub: repo': repo_url, 'Tidelift: funding': @@ -100,7 +100,7 @@ 'memcached_session': ['python-memcached>=1.58'], 'xcgi': ['flup'], - # https://docs.cherrypy.org/en/latest/advanced.html?highlight=windows#windows-console-events + # https://docs.cherrypy.dev/en/latest/advanced.html?highlight=windows#windows-console-events ':sys_platform == "win32" and implementation_name == "cpython"' # pywin32 disabled while a build is unavailable. Ref #1920. ' and python_version < "3.10"': [ From 2d11e98421f7e30d56ec523e2fb9d8d195fb7f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krystian=20Rosi=C5=84ski?= Date: Mon, 13 Sep 2021 21:32:08 +0200 Subject: [PATCH 015/322] Replace the pdf with the one compiled by pdflatex --- cherrypy/tutorial/pdf_file.pdf | Bin 85698 -> 11961 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/cherrypy/tutorial/pdf_file.pdf b/cherrypy/tutorial/pdf_file.pdf index 38b4f15eabdd65d4a674cb32034361245aa7b97e..226dfe18f73406a8f56c1a4b3508ca5e3601853f 100644 GIT binary patch literal 11961 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

    mFekkHh{DNXc%=j|!o`1gE$!{Z z;CwNFQH&1^1cQLQAWkq4!V3bj0Kp7EAOl=a-rnSYsi-*`zI$hC0%vv&ZD4RrSV>J> zi(SIm#>U9d&h9ZJH494^0RH(iG`2rO18}kPumb^%e@DplkBaE#XljN5r(nSt!2i48 zye)(i0x$#oEd%qwH}UZT*!`aj1ch(PzhpoV{Gt4Z3g1@Z6KOGqVSP{sOM0#Q-`2KpqKEaWF3k$_*A56?qNj7USaP<%WoYc(^6R jp+GSq!2ed!d&I0^PKJ(7f9yR71mWhwpr;pCl)(6ZwXwED literal 85698 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 Date: Mon, 13 Sep 2021 22:04:17 +0200 Subject: [PATCH 016/322] Fix tutorial test --- cherrypy/test/test_tutorials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cherrypy/test/test_tutorials.py b/cherrypy/test/test_tutorials.py index 39ca4d6f2..409fb7c2e 100644 --- a/cherrypy/test/test_tutorials.py +++ b/cherrypy/test/test_tutorials.py @@ -166,7 +166,7 @@ def test09Files(self): self.assertHeader('Content-Disposition', # Make sure the filename is quoted. 'attachment; filename="pdf_file.pdf"') - self.assertEqual(len(self.body), 85698) + self.assertEqual(len(self.body), 11961) def test10HTTPErrors(self): self.setup_tutorial('tut10_http_errors', 'HTTPErrorDemo') From 8245a74aa4e090c40445535a9ce3997ed9904798 Mon Sep 17 00:00:00 2001 From: Dominic Davis-Foster Date: Fri, 28 Jan 2022 23:11:52 +0000 Subject: [PATCH 017/322] Switch from inspect.getargspec to inspect.getfullargspec inspect.getargspec has been deprecated since 3.0 --- cherrypy/_cpdispatch.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cherrypy/_cpdispatch.py b/cherrypy/_cpdispatch.py index 83eb79cbe..5c506e997 100644 --- a/cherrypy/_cpdispatch.py +++ b/cherrypy/_cpdispatch.py @@ -206,12 +206,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs): def test_callable_spec(callable, args, kwargs): # noqa: F811 return None else: - getargspec = inspect.getargspec - # Python 3 requires using getfullargspec if - # keyword-only arguments are present - if hasattr(inspect, 'getfullargspec'): - def getargspec(callable): - return inspect.getfullargspec(callable)[:4] + def getargspec(callable): + return inspect.getfullargspec(callable)[:4] class LateParamPageHandler(PageHandler): From 525022150f88148a3b9feef432c0077e34e25b25 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 17:58:13 +0000 Subject: [PATCH 018/322] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - git://github.com/Lucas-C/pre-commit-hooks: v1.1.1 → v1.1.11 - git://github.com/Lucas-C/pre-commit-hooks-lxml: v1.0.2 → v1.1.0 --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9f4d43dce..9277d83c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,11 +51,11 @@ repos: ) - repo: git://github.com/Lucas-C/pre-commit-hooks - rev: v1.1.1 + rev: v1.1.11 hooks: - id: remove-tabs - repo: git://github.com/Lucas-C/pre-commit-hooks-lxml - rev: v1.0.2 + rev: v1.1.0 hooks: - id: forbid-html-img-without-alt-text From 7daf4855f58cd79e72196d3a5a0b0a860d782ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Mon, 31 Jan 2022 22:25:34 +0100 Subject: [PATCH 019/322] Use context managers to close files properly and fix tests on PyPy Use context managers (`with`) to ensure that all open files are closed correctly. This resolves resource leaks and test failures with PyPy3.7. The code prior to this change used four approaches for closing files: 1. Using a context manager (`with` clause). 2. Using a try/finally clause. 3. Closing the file in the same scope (unreliable: file object can leak on exception). 4. Not closing open files at all. The last point is a real problem for PyPy since it does not GC unreachable objects as aggressively as CPython does. While leaving a function scope on CPython causes the file objects private to it to be destroyed (and therefore closed), in PyPy they can stay dangling for some time. When combines with buffered writes, this means that writes can still remain pending after returning from function. Using a context manager is a simple, consistent way to ensure that the file object is closed once it is no longer needed. In turn, this guarantees that all pending writes will be performed upon function return and the code won't be hiting race conditions between writing a file and reading it afterwards. --- cherrypy/_cperror.py | 3 ++- cherrypy/_cpmodpy.py | 5 +---- cherrypy/lib/auth_digest.py | 13 ++++++------- cherrypy/lib/covercp.py | 5 +++-- cherrypy/lib/reprconf.py | 5 +---- cherrypy/lib/sessions.py | 10 ++-------- cherrypy/process/plugins.py | 3 ++- cherrypy/test/helper.py | 3 ++- cherrypy/test/logtest.py | 33 +++++++++++++++++++-------------- cherrypy/test/modfastcgi.py | 5 +---- cherrypy/test/modfcgid.py | 5 +---- cherrypy/test/modpy.py | 5 +---- cherrypy/test/modwsgi.py | 5 +---- cherrypy/test/test_core.py | 5 ++--- cherrypy/test/test_states.py | 11 ++++++----- 15 files changed, 50 insertions(+), 66 deletions(-) diff --git a/cherrypy/_cperror.py b/cherrypy/_cperror.py index 4e7276827..ebf1dcf61 100644 --- a/cherrypy/_cperror.py +++ b/cherrypy/_cperror.py @@ -532,7 +532,8 @@ def get_error_page(status, **kwargs): return result else: # Load the template from this path. - template = io.open(error_page, newline='').read() + with io.open(error_page, newline='') as f: + template = f.read() except Exception: e = _format_exception(*_exc_info())[-1] m = kwargs['message'] diff --git a/cherrypy/_cpmodpy.py b/cherrypy/_cpmodpy.py index 0e608c48a..a08f0ed9a 100644 --- a/cherrypy/_cpmodpy.py +++ b/cherrypy/_cpmodpy.py @@ -339,11 +339,8 @@ def start(self): } mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf') - f = open(mpconf, 'wb') - try: + with open(mpconf, 'wb') as f: f.write(conf_data) - finally: - f.close() response = read_process(self.apache_path, '-k start -f %s' % mpconf) self.ready = True diff --git a/cherrypy/lib/auth_digest.py b/cherrypy/lib/auth_digest.py index fbb5df64a..981e9a5d3 100644 --- a/cherrypy/lib/auth_digest.py +++ b/cherrypy/lib/auth_digest.py @@ -101,13 +101,12 @@ def get_ha1_file_htdigest(filename): """ def get_ha1(realm, username): result = None - f = open(filename, 'r') - for line in f: - u, r, ha1 = line.rstrip().split(':') - if u == username and r == realm: - result = ha1 - break - f.close() + with open(filename, 'r') as f: + for line in f: + u, r, ha1 = line.rstrip().split(':') + if u == username and r == realm: + result = ha1 + break return result return get_ha1 diff --git a/cherrypy/lib/covercp.py b/cherrypy/lib/covercp.py index 3e2197137..6c3871fc9 100644 --- a/cherrypy/lib/covercp.py +++ b/cherrypy/lib/covercp.py @@ -334,9 +334,10 @@ def menu(self, base='/', pct='50', showpct='', yield '' def annotated_file(self, filename, statements, excluded, missing): - source = open(filename, 'r') + with open(filename, 'r') as source: + lines = source.readlines() buffer = [] - for lineno, line in enumerate(source.readlines()): + for lineno, line in enumerate(lines): lineno += 1 line = line.strip('\n\r') empty_the_buffer = True diff --git a/cherrypy/lib/reprconf.py b/cherrypy/lib/reprconf.py index 3976652e1..76381d7b7 100644 --- a/cherrypy/lib/reprconf.py +++ b/cherrypy/lib/reprconf.py @@ -163,11 +163,8 @@ def read(self, filenames): # fp = open(filename) # except IOError: # continue - fp = open(filename) - try: + with open(filename) as fp: self._read(fp, filename) - finally: - fp.close() def as_dict(self, raw=False, vars=None): """Convert an INI file to a dictionary""" diff --git a/cherrypy/lib/sessions.py b/cherrypy/lib/sessions.py index 5b3328f2d..0f56a4fa5 100644 --- a/cherrypy/lib/sessions.py +++ b/cherrypy/lib/sessions.py @@ -516,11 +516,8 @@ def _load(self, path=None): if path is None: path = self._get_file_path() try: - f = open(path, 'rb') - try: + with open(path, 'rb') as f: return pickle.load(f) - finally: - f.close() except (IOError, EOFError): e = sys.exc_info()[1] if self.debug: @@ -531,11 +528,8 @@ def _load(self, path=None): def _save(self, expiration_time): assert self.locked, ('The session was saved without being locked. ' "Check your tools' priority levels.") - f = open(self._get_file_path(), 'wb') - try: + with open(self._get_file_path(), 'wb') as f: pickle.dump((self._data, expiration_time), f, self.pickle_protocol) - finally: - f.close() def _delete(self): assert self.locked, ('The session deletion without being locked. ' diff --git a/cherrypy/process/plugins.py b/cherrypy/process/plugins.py index 2a9952de1..e96fb1ce2 100644 --- a/cherrypy/process/plugins.py +++ b/cherrypy/process/plugins.py @@ -436,7 +436,8 @@ def start(self): if self.finalized: self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) else: - open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8')) + with open(self.pidfile, 'wb') as f: + f.write(ntob('%s\n' % pid, 'utf8')) self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) self.finalized = True start.priority = 70 diff --git a/cherrypy/test/helper.py b/cherrypy/test/helper.py index c1ca45353..cae495336 100644 --- a/cherrypy/test/helper.py +++ b/cherrypy/test/helper.py @@ -505,7 +505,8 @@ def start(self, imports=None): def get_pid(self): if self.daemonize: - return int(open(self.pid_file, 'rb').read()) + with open(self.pid_file, 'rb') as f: + return int(f.read()) return self._proc.pid def join(self): diff --git a/cherrypy/test/logtest.py b/cherrypy/test/logtest.py index 344be9877..112bdc259 100644 --- a/cherrypy/test/logtest.py +++ b/cherrypy/test/logtest.py @@ -97,7 +97,8 @@ def exit(self): def emptyLog(self): """Overwrite self.logfile with 0 bytes.""" - open(self.logfile, 'wb').write('') + with open(self.logfile, 'wb') as f: + f.write('') def markLog(self, key=None): """Insert a marker line into the log and set self.lastmarker.""" @@ -105,10 +106,11 @@ def markLog(self, key=None): key = str(time.time()) self.lastmarker = key - open(self.logfile, 'ab+').write( - b'%s%s\n' - % (self.markerPrefix, key.encode('utf-8')) - ) + with open(self.logfile, 'ab+') as f: + f.write( + 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. @@ -122,20 +124,23 @@ def _read_marked_region(self, marker=None): logfile = self.logfile marker = marker or self.lastmarker if marker is None: - return open(logfile, 'rb').readlines() + with open(logfile, 'rb') as f: + return f.readlines() if isinstance(marker, str): marker = marker.encode('utf-8') data = [] in_region = False - for line in open(logfile, 'rb'): - if in_region: - if line.startswith(self.markerPrefix) and marker not in line: - break - else: - data.append(line) - elif marker in line: - in_region = True + with open(logfile, 'rb') as f: + for line in f: + if in_region: + if (line.startswith(self.markerPrefix) + and marker not in line): + break + else: + data.append(line) + elif marker in line: + in_region = True return data def assertInLog(self, line, marker=None): diff --git a/cherrypy/test/modfastcgi.py b/cherrypy/test/modfastcgi.py index 79ec3d182..0c6d01e2c 100644 --- a/cherrypy/test/modfastcgi.py +++ b/cherrypy/test/modfastcgi.py @@ -112,15 +112,12 @@ def start_apache(self): fcgiconf = os.path.join(curdir, fcgiconf) # Write the Apache conf file. - f = open(fcgiconf, 'wb') - try: + with open(fcgiconf, 'wb') as f: server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] output = self.template % {'port': self.port, 'root': curdir, 'server': server} output = output.replace('\r\n', '\n') f.write(output) - finally: - f.close() result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) if result: diff --git a/cherrypy/test/modfcgid.py b/cherrypy/test/modfcgid.py index d101bd67f..ea373004f 100644 --- a/cherrypy/test/modfcgid.py +++ b/cherrypy/test/modfcgid.py @@ -101,15 +101,12 @@ def start_apache(self): fcgiconf = os.path.join(curdir, fcgiconf) # Write the Apache conf file. - f = open(fcgiconf, 'wb') - try: + with open(fcgiconf, 'wb') as f: server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] output = self.template % {'port': self.port, 'root': curdir, 'server': server} output = ntob(output.replace('\r\n', '\n')) f.write(output) - finally: - f.close() result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) if result: diff --git a/cherrypy/test/modpy.py b/cherrypy/test/modpy.py index 7c288d2c0..024453e99 100644 --- a/cherrypy/test/modpy.py +++ b/cherrypy/test/modpy.py @@ -107,13 +107,10 @@ def start(self, modulename): if not os.path.isabs(mpconf): mpconf = os.path.join(curdir, mpconf) - f = open(mpconf, 'wb') - try: + with open(mpconf, 'wb') as f: f.write(self.template % {'port': self.port, 'modulename': modulename, 'host': self.host}) - finally: - f.close() result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) if result: diff --git a/cherrypy/test/modwsgi.py b/cherrypy/test/modwsgi.py index da7d240b5..24c726842 100644 --- a/cherrypy/test/modwsgi.py +++ b/cherrypy/test/modwsgi.py @@ -109,14 +109,11 @@ def start(self, modulename): if not os.path.isabs(mpconf): mpconf = os.path.join(curdir, mpconf) - f = open(mpconf, 'wb') - try: + with open(mpconf, 'wb') as f: output = (self.template % {'port': self.port, 'testmod': modulename, 'curdir': curdir}) f.write(output) - finally: - f.close() result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) if result: diff --git a/cherrypy/test/test_core.py b/cherrypy/test/test_core.py index 6fde3a973..42460b3f4 100644 --- a/cherrypy/test/test_core.py +++ b/cherrypy/test/test_core.py @@ -586,9 +586,8 @@ def testRanges(self): def testFavicon(self): # favicon.ico is served by staticfile. icofilename = os.path.join(localDir, '../favicon.ico') - icofile = open(icofilename, 'rb') - data = icofile.read() - icofile.close() + with open(icofilename, 'rb') as icofile: + data = icofile.read() self.getPage('/favicon.ico') self.assertBody(data) diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py index 28dd65100..d59a4d87b 100644 --- a/cherrypy/test/test_states.py +++ b/cherrypy/test/test_states.py @@ -424,11 +424,12 @@ def test_signal_handler_unsubscribe(self): p.join() # Assert the old handler ran. - log_lines = list(open(p.error_log, 'rb')) - assert any( - line.endswith(b'I am an old SIGTERM handler.\n') - for line in log_lines - ) + with open(p.error_log, 'rb') as f: + log_lines = list(f) + assert any( + line.endswith(b'I am an old SIGTERM handler.\n') + for line in log_lines + ) def test_safe_wait_INADDR_ANY(): # pylint: disable=invalid-name From 2c6b64086c6d62e454954ed7cb32b1fc08733cd4 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:04:13 +0100 Subject: [PATCH 020/322] Use the new cherrypy.dev domain in the dist meta Ref #1872 --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index c617526ca..bd1e38f1c 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ use_scm_version=True, description='Object-Oriented HTTP framework', author='CherryPy Team', - author_email='team@cherrypy.org', + author_email='team@cherrypy.dev', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', @@ -40,12 +40,12 @@ 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', 'Topic :: Software Development :: Libraries :: Application Frameworks', ], - url='https://www.cherrypy.org', + url='https://www.cherrypy.dev', project_urls={ 'CI: AppVeyor': 'https://ci.appveyor.com/project/{}'.format(repo_slug), 'CI: Travis': 'https://travis-ci.org/{}'.format(repo_slug), 'CI: Circle': 'https://circleci.com/gh/{}'.format(repo_slug), - 'Docs: RTD': 'https://docs.cherrypy.org', + 'Docs: RTD': 'https://docs.cherrypy.dev', 'GitHub: issues': '{}/issues'.format(repo_url), 'GitHub: repo': repo_url, 'Tidelift: funding': @@ -100,7 +100,7 @@ 'memcached_session': ['python-memcached>=1.58'], 'xcgi': ['flup'], - # https://docs.cherrypy.org/en/latest/advanced.html?highlight=windows#windows-console-events + # https://docs.cherrypy.dev/en/latest/advanced.html?highlight=windows#windows-console-events ':sys_platform == "win32" and implementation_name == "cpython"' # pywin32 disabled while a build is unavailable. Ref #1920. ' and python_version < "3.10"': [ From a2641534ffbcbc5ea61af1cb35f0f945ee705ccd Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:10:27 +0100 Subject: [PATCH 021/322] Adopt two-space indents in `pytest.ini` --- pytest.ini | 88 +++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/pytest.ini b/pytest.ini index 89197092d..ff5bc4615 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,64 +1,64 @@ [pytest] addopts = - # `pytest-xdist`: - #--numprocesses=auto + # `pytest-xdist`: + # --numprocesses=auto - # `pytest-mon`: - # useful for live testing with `pytest-watch` during development: - #--testmon + # `pytest-mon`: + # useful for live testing with `pytest-watch` during development: + #--testmon - # show 10 slowest invocations: - --durations=10 + # show 10 slowest invocations: + --durations=10 - # a bit of verbosity doesn't hurt: - -v + # a bit of verbosity doesn't hurt: + -v - # report all the things == -rxXs: - -ra + # report all the things == -rxXs: + -ra - # show values of the local vars in errors: - --showlocals + # show values of the local vars in errors: + --showlocals - # autocollect and invoke the doctests from all modules: - --doctest-modules + # autocollect and invoke the doctests from all modules: + --doctest-modules - # dump the test results in junit format: - --junitxml=.test-results/pytest/results.xml + # dump the test results in junit format: + --junitxml=.test-results/pytest/results.xml - # `pytest-cov`: - --cov=cherrypy - --cov-report term-missing:skip-covered - --cov-report xml - # --cov-report xml:.test-results/pytest/cov.xml # alternatively move it here + # `pytest-cov`: + --cov=cherrypy + --cov-report=term-missing:skip-covered + --cov-report=xml + # --cov-report xml:.test-results/pytest/cov.xml # alternatively move it here doctest_optionflags = ALLOW_UNICODE ELLIPSIS filterwarnings = - error + error - # pytest>=6.2.0 under Python 3.8: - # Ref: https://docs.pytest.org/en/stable/usage.html#unraisable - # Ref: https://github.com/pytest-dev/pytest/issues/5299 - ignore:Exception ignored in. :pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception - ignore:Exception ignored in. <_io.FileIO .closed.>:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception + # pytest>=6.2.0 under Python 3.8: + # Ref: https://docs.pytest.org/en/stable/usage.html#unraisable + # Ref: https://github.com/pytest-dev/pytest/issues/5299 + ignore:Exception ignored in. :pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception + ignore:Exception ignored in. <_io.FileIO .closed.>:pytest.PytestUnraisableExceptionWarning:_pytest.unraisableexception - ignore:Use cheroot.test.webtest:DeprecationWarning - ignore:This method will be removed in future versions.*:DeprecationWarning - ignore:Unable to verify that the server is bound on:UserWarning - ignore:Not importing directory .*.tox/py35/lib/python3.5/site-packages/(zc|repoze).* missing __init__:ImportWarning - # ref: https://github.com/mhammond/pywin32/issues/1256#issuecomment-527972824 : - ignore:the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses:DeprecationWarning - ignore:the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses:PendingDeprecationWarning + ignore:Use cheroot.test.webtest:DeprecationWarning + ignore:This method will be removed in future versions.*:DeprecationWarning + ignore:Unable to verify that the server is bound on:UserWarning + ignore:Not importing directory .*.tox/py35/lib/python3.5/site-packages/(zc|repoze).* missing __init__:ImportWarning + # ref: https://github.com/mhammond/pywin32/issues/1256#issuecomment-527972824 : + ignore:the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses:DeprecationWarning + ignore:the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses:PendingDeprecationWarning junit_duration_report = call junit_family = xunit2 junit_suite_name = cherrypy_test_suite minversion = 5.3.5 norecursedirs = - build - cherrypy.egg-info - dist - docs - .cache - .eggs - .git - .github - .tox + build + cherrypy.egg-info + dist + docs + .cache + .eggs + .git + .github + .tox testpaths = cherrypy/test/ From 17cf35da453f7d56f01e38ae3e295836be8947b5 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:11:54 +0100 Subject: [PATCH 022/322] Make settings in `pytest.ini` sparse --- pytest.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pytest.ini b/pytest.ini index ff5bc4615..97415a274 100644 --- a/pytest.ini +++ b/pytest.ini @@ -30,7 +30,9 @@ addopts = --cov-report=term-missing:skip-covered --cov-report=xml # --cov-report xml:.test-results/pytest/cov.xml # alternatively move it here + doctest_optionflags = ALLOW_UNICODE ELLIPSIS + filterwarnings = error @@ -47,10 +49,13 @@ filterwarnings = # ref: https://github.com/mhammond/pywin32/issues/1256#issuecomment-527972824 : ignore:the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses:DeprecationWarning ignore:the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses:PendingDeprecationWarning + junit_duration_report = call junit_family = xunit2 junit_suite_name = cherrypy_test_suite + minversion = 5.3.5 + norecursedirs = build cherrypy.egg-info @@ -61,4 +66,5 @@ norecursedirs = .git .github .tox + testpaths = cherrypy/test/ From 565c181ff2b26e4685d36f500f78c99e04091590 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:26:01 +0100 Subject: [PATCH 023/322] Make xfail mode strict in pytest --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index 97415a274..6147e5a63 100644 --- a/pytest.ini +++ b/pytest.ini @@ -68,3 +68,5 @@ norecursedirs = .tox testpaths = cherrypy/test/ + +xfail_strict = true From 13aa7e8683b8ccbafaff2a10eb93ba78d829142f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:26:37 +0100 Subject: [PATCH 024/322] Declare an empty markers list in pytest config --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index 6147e5a63..320e77f4a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -54,6 +54,9 @@ junit_duration_report = call junit_family = xunit2 junit_suite_name = cherrypy_test_suite +# A mapping of markers to their descriptions allowed in strict mode: +markers = + minversion = 5.3.5 norecursedirs = From be993e1101ba40443c2b825d8675fccc901eb935 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:27:13 +0100 Subject: [PATCH 025/322] Annotate usage of norecursedirs in pytest config --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index 320e77f4a..3df37083c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -59,6 +59,7 @@ markers = minversion = 5.3.5 +# Optimize pytest's lookup by restricting potentially deep dir tree scan: norecursedirs = build cherrypy.egg-info From b43ba31cf23fbde49eeca17a7562173200b4cc8b Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:27:42 +0100 Subject: [PATCH 026/322] Add FIXME to commented out xdist setting @ pytest --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index 3df37083c..9335b27b4 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,6 @@ [pytest] addopts = + # FIXME: Enable this once the test suite has no race conditions # `pytest-xdist`: # --numprocesses=auto From 303d5ee44f46ff2aba499679573b86b94e4a6809 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:28:45 +0100 Subject: [PATCH 027/322] Titlecase the annotation comments in pytest config --- pytest.ini | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pytest.ini b/pytest.ini index 9335b27b4..4df7b5f50 100644 --- a/pytest.ini +++ b/pytest.ini @@ -5,25 +5,25 @@ addopts = # --numprocesses=auto # `pytest-mon`: - # useful for live testing with `pytest-watch` during development: + # Useful for live testing with `pytest-watch` during development: #--testmon - # show 10 slowest invocations: + # Show 10 slowest invocations: --durations=10 - # a bit of verbosity doesn't hurt: + # A bit of verbosity doesn't hurt: -v - # report all the things == -rxXs: + # Report all the things == -rxXs: -ra - # show values of the local vars in errors: + # Show values of the local vars in errors: --showlocals - # autocollect and invoke the doctests from all modules: + # Autocollect and invoke the doctests from all modules: --doctest-modules - # dump the test results in junit format: + # Dump the test results in junit format: --junitxml=.test-results/pytest/results.xml # `pytest-cov`: From 58dcca2dab27ca4efff50db893343bc7d4626bd2 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:29:27 +0100 Subject: [PATCH 028/322] Add a doctest pointer link to pytest config --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index 4df7b5f50..d8a95ae29 100644 --- a/pytest.ini +++ b/pytest.ini @@ -21,6 +21,7 @@ addopts = --showlocals # Autocollect and invoke the doctests from all modules: + # https://docs.pytest.org/en/stable/doctest.html --doctest-modules # Dump the test results in junit format: From c88107ac79f585de7f2866a07555f9a5c5abab87 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:30:05 +0100 Subject: [PATCH 029/322] Pre-load `pytest-cov` plugin early --- pytest.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pytest.ini b/pytest.ini index d8a95ae29..75c596f59 100644 --- a/pytest.ini +++ b/pytest.ini @@ -28,6 +28,8 @@ addopts = --junitxml=.test-results/pytest/results.xml # `pytest-cov`: + # `pytest-cov`, "-p" preloads the module early: + -p pytest_cov --cov=cherrypy --cov-report=term-missing:skip-covered --cov-report=xml From 18a41b62740ca41c4f12b56a9469f43eb6032fbc Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:30:33 +0100 Subject: [PATCH 030/322] Tell pytest to hide coverage on failures --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index 75c596f59..ed0710b10 100644 --- a/pytest.ini +++ b/pytest.ini @@ -30,6 +30,7 @@ addopts = # `pytest-cov`: # `pytest-cov`, "-p" preloads the module early: -p pytest_cov + --no-cov-on-fail --cov=cherrypy --cov-report=term-missing:skip-covered --cov-report=xml From ebd06b544f2115e58933acbf589de2685450bbc5 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:31:22 +0100 Subject: [PATCH 031/322] Tell pytest-cov to measure branche coverage --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index ed0710b10..2bab50bd9 100644 --- a/pytest.ini +++ b/pytest.ini @@ -32,6 +32,7 @@ addopts = -p pytest_cov --no-cov-on-fail --cov=cherrypy + --cov-branch --cov-report=term-missing:skip-covered --cov-report=xml # --cov-report xml:.test-results/pytest/cov.xml # alternatively move it here From 533e2893c392dc06b04e03683c6869f65cb46c97 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:32:04 +0100 Subject: [PATCH 032/322] Output an HTML coverage report from pytest --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index 2bab50bd9..b5fd53d30 100644 --- a/pytest.ini +++ b/pytest.ini @@ -34,6 +34,7 @@ addopts = --cov=cherrypy --cov-branch --cov-report=term-missing:skip-covered + --cov-report=html:.tox/tmp/test-results/pytest/cov/ --cov-report=xml # --cov-report xml:.test-results/pytest/cov.xml # alternatively move it here From 7f71dbd9a7ebbd016a93eff82ab82d844eff4283 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:32:33 +0100 Subject: [PATCH 033/322] Set coveragepy context to `test` via `pytest-cov` --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index b5fd53d30..e0e64b450 100644 --- a/pytest.ini +++ b/pytest.ini @@ -37,6 +37,7 @@ addopts = --cov-report=html:.tox/tmp/test-results/pytest/cov/ --cov-report=xml # --cov-report xml:.test-results/pytest/cov.xml # alternatively move it here + --cov-context=test doctest_optionflags = ALLOW_UNICODE ELLIPSIS From 36551c95fd84279ff8dd95b38a4ef8032bbd9d81 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:32:59 +0100 Subject: [PATCH 034/322] Set explicit coverage config to use @ `pytest-cov` --- pytest.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest.ini b/pytest.ini index e0e64b450..bc8f53bb8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -38,6 +38,7 @@ addopts = --cov-report=xml # --cov-report xml:.test-results/pytest/cov.xml # alternatively move it here --cov-context=test + --cov-config=.coveragerc doctest_optionflags = ALLOW_UNICODE ELLIPSIS From e430f92773f2b3689fb924b6ceca2828e3f45178 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:36:05 +0100 Subject: [PATCH 035/322] Upgrade pre-commit to use HTTPS Git URLs --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9277d83c5..2a778d48d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: -- repo: git://github.com/pre-commit/pre-commit-hooks +- repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.1.1 hooks: - id: trailing-whitespace @@ -28,7 +28,7 @@ repos: exclude: cherrypy/test/test.pem - id: requirements-txt-fixer -- repo: git://github.com/chewse/pre-commit-mirrors-pydocstyle +- repo: https://github.com/chewse/pre-commit-mirrors-pydocstyle rev: v2.1.1 hooks: - id: pydocstyle @@ -50,12 +50,12 @@ repos: test|tutorial ) -- repo: git://github.com/Lucas-C/pre-commit-hooks +- repo: https://github.com/Lucas-C/pre-commit-hooks rev: v1.1.11 hooks: - id: remove-tabs -- repo: git://github.com/Lucas-C/pre-commit-hooks-lxml +- repo: https://github.com/Lucas-C/pre-commit-hooks-lxml rev: v1.1.0 hooks: - id: forbid-html-img-without-alt-text From a68be36762808848c99cac965fd17079b8ce4eb1 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:36:44 +0100 Subject: [PATCH 036/322] Use a traditional `.git` suffix in pre-commit conf --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2a778d48d..3da3a4e8d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: -- repo: https://github.com/pre-commit/pre-commit-hooks +- repo: https://github.com/pre-commit/pre-commit-hooks.git rev: v1.1.1 hooks: - id: trailing-whitespace @@ -28,7 +28,7 @@ repos: exclude: cherrypy/test/test.pem - id: requirements-txt-fixer -- repo: https://github.com/chewse/pre-commit-mirrors-pydocstyle +- repo: https://github.com/chewse/pre-commit-mirrors-pydocstyle.git rev: v2.1.1 hooks: - id: pydocstyle @@ -50,12 +50,12 @@ repos: test|tutorial ) -- repo: https://github.com/Lucas-C/pre-commit-hooks +- repo: https://github.com/Lucas-C/pre-commit-hooks.git rev: v1.1.11 hooks: - id: remove-tabs -- repo: https://github.com/Lucas-C/pre-commit-hooks-lxml +- repo: https://github.com/Lucas-C/pre-commit-hooks-lxml.git rev: v1.1.0 hooks: - id: forbid-html-img-without-alt-text From 02d19e7d69d3cb536a279fea059f3a2d76be1e2c Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 1 Feb 2022 19:40:18 +0100 Subject: [PATCH 037/322] Add a config for the Patchback robot --- .github/patchback.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/patchback.yml diff --git a/.github/patchback.yml b/.github/patchback.yml new file mode 100644 index 000000000..18bd840b4 --- /dev/null +++ b/.github/patchback.yml @@ -0,0 +1,5 @@ +--- +backport_branch_prefix: patchback/backports/ +backport_label_prefix: backport- +target_branch_prefix: maint/ +... From 27774c9289e4d3b7d2fd8a87a8ff137ee37bef3d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 17:59:56 +0000 Subject: [PATCH 038/322] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/Lucas-C/pre-commit-hooks.git: v1.1.11 → v1.1.12](https://github.com/Lucas-C/pre-commit-hooks.git/compare/v1.1.11...v1.1.12) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3da3a4e8d..a44f278c0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,7 +51,7 @@ repos: ) - repo: https://github.com/Lucas-C/pre-commit-hooks.git - rev: v1.1.11 + rev: v1.1.12 hooks: - id: remove-tabs From 5fff5e4b3aea99b051a55f813b7ddd194425eb2d Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sun, 13 Mar 2022 23:31:07 +0100 Subject: [PATCH 039/322] Add a #StandWithUkraine banner to the README --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index d7b4b2474..1a2892a8d 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,7 @@ +.. image:: https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg + :target: https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md + :alt: SWUbanner + .. image:: https://img.shields.io/pypi/v/cherrypy.svg :target: https://pypi.org/project/cherrypy From 1ba0e5e6cc16045e370cc9a9f17a96bd1bfb5157 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Jul 2022 21:27:36 -0400 Subject: [PATCH 040/322] Remove trove classifiers. No one needs this toil. --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index bd1e38f1c..c77b28fcc 100644 --- a/setup.py +++ b/setup.py @@ -25,9 +25,6 @@ 'License :: OSI Approved :: BSD License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: Jython', From 057937d48b18bf2c4782866fd53d7df244b9e725 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Jul 2022 21:30:10 -0400 Subject: [PATCH 041/322] Update changelog. --- CHANGES.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.rst b/CHANGES.rst index 9e081b5af..697d77efc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,6 +2,7 @@ v18.7.0 ------- * :pr:`1923`: Drop support for Python 3.5. +* :pr:`1945`: Fixed compatibility on Python 3.11. v18.6.1 ------- From 843eb98f2f726ab5775988a3c1a7b2013b753cfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jul 2022 09:06:29 -0400 Subject: [PATCH 042/322] Revert "Make xfail mode strict in pytest" This reverts commit 565c181ff2b26e4685d36f500f78c99e04091590. Ref #1536 --- pytest.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index bc8f53bb8..aef459ebe 100644 --- a/pytest.ini +++ b/pytest.ini @@ -81,5 +81,3 @@ norecursedirs = .tox testpaths = cherrypy/test/ - -xfail_strict = true From 31e76073e48325598fe808e300e3efd09c3030f3 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 12 Jul 2022 20:56:55 +0200 Subject: [PATCH 043/322] Fix yamllint integration with pre-commit --- .pre-commit-config.yaml | 9 +++++++++ .readthedocs.yml | 4 ++-- .yamllint | 6 ++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a44f278c0..0c75d0b0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,3 +59,12 @@ repos: rev: v1.1.0 hooks: - id: forbid-html-img-without-alt-text + +- repo: https://github.com/adrienverge/yamllint.git + rev: v1.27.1 + hooks: + - id: yamllint + files: \.(yaml|yml)$ + types: [file, yaml] + args: + - --strict diff --git a/.readthedocs.yml b/.readthedocs.yml index ee2c84c45..3eb1a2b4a 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -3,6 +3,6 @@ build: python: version: 3.6 extra_requirements: - - docs - - testing + - docs + - testing pip_install: true diff --git a/.yamllint b/.yamllint index 97c08cfcd..434c9f9b2 100644 --- a/.yamllint +++ b/.yamllint @@ -1,2 +1,4 @@ -indentation: - indent-sequences: false +rules: + indentation: + level: error + indent-sequences: false From 2296d0e885b9e575374c83724bebeb8a312c9a1e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Jul 2022 16:25:59 -0400 Subject: [PATCH 044/322] Add test capturing missed expectation. Ref #1974. --- cherrypy/test/test_request_obj.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cherrypy/test/test_request_obj.py b/cherrypy/test/test_request_obj.py index 3aaa8e817..482027be9 100644 --- a/cherrypy/test/test_request_obj.py +++ b/cherrypy/test/test_request_obj.py @@ -7,6 +7,8 @@ import uuid from http.client import IncompleteRead +import pytest + import cherrypy from cherrypy._cpcompat import ntou from cherrypy.lib import httputil @@ -756,6 +758,17 @@ def test_header_presence(self): headers=[('Content-type', 'application/json')]) self.assertBody('application/json') + @pytest.mark.xfail(reason="#1974") + def test_dangerous_host(self): + """ + Dangerous characters like newlines should be elided. + Ref #1974. + """ + # foo\nbar + encoded = '=?iso-8859-1?q?foo=0Abar?=' + self.getPage('/headers/Host', headers=[('Host', encoded)]) + self.assertBody('foobar') + def test_basic_HTTPMethods(self): helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND', 'PATCH') From 78331575dacb67eaabf3e645533f1d05b861f00d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Jul 2022 16:26:54 -0400 Subject: [PATCH 045/322] Introduce SanitizedHost wrapper. Fixes #1974. --- cherrypy/_cprequest.py | 3 +++ cherrypy/lib/httputil.py | 30 ++++++++++++++++++++++++++++++ cherrypy/test/test_request_obj.py | 3 --- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/cherrypy/_cprequest.py b/cherrypy/_cprequest.py index b380bb75a..a661112c6 100644 --- a/cherrypy/_cprequest.py +++ b/cherrypy/_cprequest.py @@ -742,6 +742,9 @@ def process_headers(self): if self.protocol >= (1, 1): msg = "HTTP/1.1 requires a 'Host' request header." raise cherrypy.HTTPError(400, msg) + else: + headers['Host'] = httputil.SanitizedHost(dict.get(headers, 'Host')) + host = dict.get(headers, 'Host') if not host: host = self.local.name or self.local.ip diff --git a/cherrypy/lib/httputil.py b/cherrypy/lib/httputil.py index eedf8d89c..ced310a0a 100644 --- a/cherrypy/lib/httputil.py +++ b/cherrypy/lib/httputil.py @@ -516,3 +516,33 @@ def __init__(self, ip, port, name=None): def __repr__(self): return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name) + + +class SanitizedHost(str): + r""" + Wraps a raw host header received from the network in + a sanitized version that elides dangerous characters. + + >>> SanitizedHost('foo\nbar') + 'foobar' + >>> SanitizedHost('foo\nbar').raw + 'foo\nbar' + + A SanitizedInstance is only returned if sanitization was performed. + + >>> isinstance(SanitizedHost('foobar'), SanitizedHost) + False + """ + dangerous = re.compile(r'[\n\r]') + + def __new__(cls, raw): + sanitized = cls._sanitize(raw) + if sanitized == raw: + return raw + instance = super().__new__(cls, sanitized) + instance.raw = raw + return instance + + @classmethod + def _sanitize(cls, raw): + return cls.dangerous.sub('', raw) diff --git a/cherrypy/test/test_request_obj.py b/cherrypy/test/test_request_obj.py index 482027be9..2478aabe5 100644 --- a/cherrypy/test/test_request_obj.py +++ b/cherrypy/test/test_request_obj.py @@ -7,8 +7,6 @@ import uuid from http.client import IncompleteRead -import pytest - import cherrypy from cherrypy._cpcompat import ntou from cherrypy.lib import httputil @@ -758,7 +756,6 @@ def test_header_presence(self): headers=[('Content-type', 'application/json')]) self.assertBody('application/json') - @pytest.mark.xfail(reason="#1974") def test_dangerous_host(self): """ Dangerous characters like newlines should be elided. From 72d94e3adf2786aaa723f3f1b5bf8a9b34f28332 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 17 Jul 2022 16:32:08 -0400 Subject: [PATCH 046/322] Update changelog. Ref #1974. --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 697d77efc..35b3f895d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +v18.8.0 +------- + +* :issue:`1974`: Dangerous characters received in a host header + encoded using RFC 2047 are now elided by default. Currently, + dangerous characters are defined as CR and LF. The original + value is still available as ``cherrypy.request.headers['Host'].raw`` + if needed. + v18.7.0 ------- From cefd0d9b8b111b355081014ec683488c104f5f12 Mon Sep 17 00:00:00 2001 From: Alexander Myltsev Date: Thu, 28 Jul 2022 16:50:06 +0300 Subject: [PATCH 047/322] Fix typo (same should be fixed on cherrypy.dev main page) --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 6914a8dde..15834b7e3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,7 +32,7 @@ CherryPy allows developers to build web applications in much the same way they would build any other object-oriented Python program. This results in smaller source code developed in less time. -CherryPy is now more than ten years old and it is has proven to +CherryPy is now more than ten years old, and it has proven to be fast and reliable. It is being used in production by many sites, from the simplest to the most demanding. From 0ad19c257b7f75597cf515c85e6973c78cbf9a15 Mon Sep 17 00:00:00 2001 From: Daniel Garcia Moreno Date: Wed, 21 Dec 2022 11:08:11 +0100 Subject: [PATCH 048/322] Remove duplicated "keep" word in issue template --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index ee3e4b860..af86b79f9 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,7 @@ \r ') + sys.stdout.write("<-- More -->\r ") m = getchar().lower() # Erase our "More" prompt - sys.stdout.write(' \r ') - if m == 'q': + sys.stdout.write(" \r ") + if m == "q": break print(line.rstrip()) - elif i == 'M': + elif i == "M": print(repr(marker or self.lastmarker)) - elif i == 'P': + elif i == "P": print(repr(pattern)) - elif i == 'I': + elif i == "I": # return without raising the normal exception return - elif i == 'R': + elif i == "R": raise pytest.fail(msg) - elif i == 'X': + elif i == "X": self.exit() - sys.stdout.write(p + ' ') + sys.stdout.write(p + " ") def exit(self): """Terminate the program.""" @@ -99,8 +101,8 @@ def exit(self): def emptyLog(self): """Overwrite self.logfile with 0 bytes.""" - with open(self.logfile, 'wb') as f: - f.write('') + with open(self.logfile, "wb") as f: + f.write("") def markLog(self, key=None): """Insert a marker line into the log and set self.lastmarker.""" @@ -108,11 +110,8 @@ def markLog(self, key=None): key = str(time.time()) self.lastmarker = key - with open(self.logfile, 'ab+') as f: - f.write( - b'%s%s\n' - % (self.markerPrefix, key.encode('utf-8')) - ) + with open(self.logfile, "ab+") as f: + f.write(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,24 +120,23 @@ def _read_marked_region(self, marker=None): been marked (using self.markLog), the entire log will be returned. """ -# Give the logger time to finish writing? -# time.sleep(0.5) + # Give the logger time to finish writing? + # time.sleep(0.5) logfile = self.logfile marker = marker or self.lastmarker if marker is None: - with open(logfile, 'rb') as f: + with open(logfile, "rb") as f: return f.readlines() if isinstance(marker, str): - marker = marker.encode('utf-8') + marker = marker.encode("utf-8") data = [] in_region = False - with open(logfile, 'rb') as f: + with open(logfile, "rb") as f: for line in f: if in_region: - if (line.startswith(self.markerPrefix) - and marker not in line): + if line.startswith(self.markerPrefix) and marker not in line: break else: data.append(line) @@ -158,7 +156,7 @@ def assertInLog(self, line, marker=None): for logline in data: if line in logline: return - msg = '%r not found in log' % line + msg = "%r not found in log" % line self._handleLogError(msg, data, marker, line) def assertNotInLog(self, line, marker=None): @@ -172,7 +170,7 @@ def assertNotInLog(self, line, marker=None): data = self._read_marked_region(marker) for logline in data: if line in logline: - msg = '%r found in log' % line + msg = "%r found in log" % line self._handleLogError(msg, data, marker, line) def assertValidUUIDv4(self, marker=None): @@ -184,10 +182,7 @@ def assertValidUUIDv4(self, marker=None): searched. """ data = self._read_marked_region(marker) - data = [ - chunk.decode('utf-8').rstrip('\n').rstrip('\r') - for chunk in data - ] + data = [chunk.decode("utf-8").rstrip("\n").rstrip("\r") for chunk in data] for log_chunk in data: try: uuid_log = data[-1] @@ -197,10 +192,10 @@ def assertValidUUIDv4(self, marker=None): else: if str(uuid_obj) == uuid_log: return - msg = '%r is not a valid UUIDv4' % uuid_log + msg = "%r is not a valid UUIDv4" % uuid_log self._handleLogError(msg, data, marker, log_chunk) - msg = 'UUIDv4 not found in log' + msg = "UUIDv4 not found in log" self._handleLogError(msg, data, marker, log_chunk) def assertLog(self, sliceargs, lines, marker=None): @@ -217,27 +212,29 @@ def assertLog(self, sliceargs, lines, marker=None): if isinstance(lines, (tuple, list)): lines = lines[0] if isinstance(lines, str): - lines = lines.encode('utf-8') + lines = lines.encode("utf-8") if lines not in data[sliceargs]: - msg = '%r not found on log line %r' % (lines, sliceargs) + msg = "%r not found on log line %r" % (lines, sliceargs) self._handleLogError( msg, - [data[sliceargs], '--EXTRA CONTEXT--'] + data[ - sliceargs + 1:sliceargs + 6], + [data[sliceargs], "--EXTRA CONTEXT--"] + + data[sliceargs + 1 : sliceargs + 6], marker, - lines) + lines, + ) else: # Multiple args. Use __getslice__ and require lines to be list. if isinstance(lines, tuple): lines = list(lines) elif isinstance(lines, text_or_bytes): - raise TypeError("The 'lines' arg must be a list when " - "'sliceargs' is a tuple.") + raise TypeError( + "The 'lines' arg must be a list when " "'sliceargs' is a tuple." + ) start, stop = sliceargs for line, logline in zip(lines, data[start:stop]): if isinstance(line, str): - line = line.encode('utf-8') + line = line.encode("utf-8") if line not in logline: - msg = '%r not found in log' % line + msg = "%r not found in log" % line self._handleLogError(msg, data[start:stop], marker, line) diff --git a/cherrypy/test/modfastcgi.py b/cherrypy/test/modfastcgi.py index 8c20605fb..e48822048 100644 --- a/cherrypy/test/modfastcgi.py +++ b/cherrypy/test/modfastcgi.py @@ -43,22 +43,23 @@ curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -def read_process(cmd, args=''): +def read_process(cmd, args=""): """Return subprocess' console output.""" - pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) try: firstline = pipeout.readline() - if (re.search(r'(not recognized|No such file|not found)', firstline, - re.IGNORECASE)): - raise IOError('%s must be on your system path.' % cmd) + if re.search( + r"(not recognized|No such file|not found)", firstline, re.IGNORECASE + ): + raise IOError("%s must be on your system path." % cmd) output = firstline + pipeout.read() finally: pipeout.close() return output -APACHE_PATH = 'apache2ctl' -CONF_PATH = 'fastcgi.conf' +APACHE_PATH = "apache2ctl" +CONF_PATH = "fastcgi.conf" conf_fastcgi = """ # Apache2 server conf file for testing CherryPy with mod_fastcgi. @@ -83,27 +84,28 @@ def read_process(cmd, args=''): def erase_script_name(environ, start_response): """Erase script name from the WSGI environment.""" - environ['SCRIPT_NAME'] = '' + environ["SCRIPT_NAME"] = "" return cherrypy.tree(environ, start_response) class ModFCGISupervisor(helper.LocalWSGISupervisor): """Server Controller for ModFastCGI and CherryPy.""" - httpserver_class = 'cherrypy.process.servers.FlupFCGIServer' + httpserver_class = "cherrypy.process.servers.FlupFCGIServer" using_apache = True using_wsgi = True template = conf_fastcgi def __str__(self): """Render a :class:`ModFastCGISupervisor` instance as a string.""" - return 'FCGI Server on %s:%s' % (self.host, self.port) + return "FCGI Server on %s:%s" % (self.host, self.port) def start(self, modulename): """Spawn an Apache ``mod_fastcgi`` supervisor process.""" cherrypy.server.httpserver = servers.FlupFCGIServer( - application=erase_script_name, bindAddress=('127.0.0.1', 4000)) - cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + application=erase_script_name, bindAddress=("127.0.0.1", 4000) + ) + cherrypy.server.httpserver.bind_addr = ("127.0.0.1", 4000) cherrypy.server.socket_port = 4000 # For FCGI, we both start apache... self.start_apache() @@ -118,23 +120,27 @@ def start_apache(self): fcgiconf = os.path.join(curdir, fcgiconf) # Write the Apache conf file. - with open(fcgiconf, 'wb') as f: - server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] - output = self.template % {'port': self.port, 'root': curdir, - 'server': server} - output = output.replace('\r\n', '\n') + with open(fcgiconf, "wb") as f: + server = repr(os.path.join(curdir, "fastcgi.pyc"))[1:-1] + output = self.template % { + "port": self.port, + "root": curdir, + "server": server, + } + output = output.replace("\r\n", "\n") f.write(output) - result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) + result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf) if result: print(result) def stop(self): """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, '-k stop') + read_process(APACHE_PATH, "-k stop") helper.LocalWSGISupervisor.stop(self) def sync_apps(self): """Set up the FastCGI request handler.""" cherrypy.server.httpserver.fcgiserver.application = self.get_app( - erase_script_name) + erase_script_name + ) diff --git a/cherrypy/test/modfcgid.py b/cherrypy/test/modfcgid.py index 5a4a3a8f1..dd51f8914 100644 --- a/cherrypy/test/modfcgid.py +++ b/cherrypy/test/modfcgid.py @@ -44,22 +44,23 @@ curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -def read_process(cmd, args=''): +def read_process(cmd, args=""): """Return subprocess' console output.""" - pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) try: firstline = pipeout.readline() - if (re.search(r'(not recognized|No such file|not found)', firstline, - re.IGNORECASE)): - raise IOError('%s must be on your system path.' % cmd) + if re.search( + r"(not recognized|No such file|not found)", firstline, re.IGNORECASE + ): + raise IOError("%s must be on your system path." % cmd) output = firstline + pipeout.read() finally: pipeout.close() return output -APACHE_PATH = 'httpd' -CONF_PATH = 'fcgi.conf' +APACHE_PATH = "httpd" +CONF_PATH = "fcgi.conf" conf_fcgid = """ # Apache2 server conf file for testing CherryPy with mod_fcgid. @@ -87,13 +88,14 @@ class ModFCGISupervisor(helper.LocalSupervisor): def __str__(self): """Render a :class:`ModFCGISupervisor` instance as a string.""" - return 'FCGI Server on %s:%s' % (self.host, self.port) + return "FCGI Server on %s:%s" % (self.host, self.port) def start(self, modulename): """Spawn an Apache ``mod_fcgid`` supervisor process.""" cherrypy.server.httpserver = servers.FlupFCGIServer( - application=cherrypy.tree, bindAddress=('127.0.0.1', 4000)) - cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + application=cherrypy.tree, bindAddress=("127.0.0.1", 4000) + ) + cherrypy.server.httpserver.bind_addr = ("127.0.0.1", 4000) # For FCGI, we both start apache... self.start_apache() # ...and our local server @@ -106,20 +108,23 @@ def start_apache(self): fcgiconf = os.path.join(curdir, fcgiconf) # Write the Apache conf file. - with open(fcgiconf, 'wb') as f: - server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] - output = self.template % {'port': self.port, 'root': curdir, - 'server': server} - output = ntob(output.replace('\r\n', '\n')) + with open(fcgiconf, "wb") as f: + server = repr(os.path.join(curdir, "fastcgi.pyc"))[1:-1] + output = self.template % { + "port": self.port, + "root": curdir, + "server": server, + } + output = ntob(output.replace("\r\n", "\n")) f.write(output) - result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) + result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf) if result: print(result) def stop(self): """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, '-k stop') + read_process(APACHE_PATH, "-k stop") helper.LocalServer.stop(self) def sync_apps(self): diff --git a/cherrypy/test/modpy.py b/cherrypy/test/modpy.py index c9b3062b0..a7e2d52a4 100644 --- a/cherrypy/test/modpy.py +++ b/cherrypy/test/modpy.py @@ -43,22 +43,23 @@ curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -def read_process(cmd, args=''): +def read_process(cmd, args=""): """Return subprocess' console output.""" - pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) try: firstline = pipeout.readline() - if (re.search(r'(not recognized|No such file|not found)', firstline, - re.IGNORECASE)): - raise IOError('%s must be on your system path.' % cmd) + if re.search( + r"(not recognized|No such file|not found)", firstline, re.IGNORECASE + ): + raise IOError("%s must be on your system path." % cmd) output = firstline + pipeout.read() finally: pipeout.close() return output -APACHE_PATH = 'httpd' -CONF_PATH = 'test_mp.conf' +APACHE_PATH = "httpd" +CONF_PATH = "test_mp.conf" conf_modpython_gateway = """ # Apache2 server conf file for testing CherryPy with modpython_gateway. @@ -103,7 +104,7 @@ class ModPythonSupervisor(helper.Supervisor): def __str__(self): """Render a :class:`ModPythonSupervisor` instance as a string.""" - return 'ModPython Server on %s:%s' % (self.host, self.port) + return "ModPython Server on %s:%s" % (self.host, self.port) def start(self, modulename): """Spawn an Apache ``mod_python`` supervisor process.""" @@ -111,18 +112,19 @@ def start(self, modulename): if not os.path.isabs(mpconf): mpconf = os.path.join(curdir, mpconf) - with open(mpconf, 'wb') as f: - f.write(self.template % - {'port': self.port, 'modulename': modulename, - 'host': self.host}) + with open(mpconf, "wb") as f: + f.write( + self.template + % {"port": self.port, "modulename": modulename, "host": self.host} + ) - result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) + result = read_process(APACHE_PATH, "-k start -f %s" % mpconf) if result: print(result) def stop(self): """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, '-k stop') + read_process(APACHE_PATH, "-k stop") loaded = False @@ -135,19 +137,22 @@ def wsgisetup(req): loaded = True options = req.get_options() - cherrypy.config.update({ - 'log.error_file': os.path.join(curdir, 'test.log'), - 'environment': 'test_suite', - 'server.socket_host': options['socket_host'], - }) + cherrypy.config.update( + { + "log.error_file": os.path.join(curdir, "test.log"), + "environment": "test_suite", + "server.socket_host": options["socket_host"], + } + ) - modname = options['testmod'] - mod = __import__(modname, globals(), locals(), ['']) + modname = options["testmod"] + mod = __import__(modname, globals(), locals(), [""]) mod.setup_server() cherrypy.server.unsubscribe() cherrypy.engine.start() from mod_python import apache + return apache.OK @@ -158,10 +163,13 @@ def cpmodpysetup(req): loaded = True options = req.get_options() - cherrypy.config.update({ - 'log.error_file': os.path.join(curdir, 'test.log'), - 'environment': 'test_suite', - 'server.socket_host': options['socket_host'], - }) + cherrypy.config.update( + { + "log.error_file": os.path.join(curdir, "test.log"), + "environment": "test_suite", + "server.socket_host": options["socket_host"], + } + ) from mod_python import apache + return apache.OK diff --git a/cherrypy/test/modwsgi.py b/cherrypy/test/modwsgi.py index 1e074e63b..12b19359b 100644 --- a/cherrypy/test/modwsgi.py +++ b/cherrypy/test/modwsgi.py @@ -47,26 +47,27 @@ curdir = os.path.abspath(os.path.dirname(__file__)) -def read_process(cmd, args=''): +def read_process(cmd, args=""): """Return subprocess' console output.""" - pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) try: firstline = pipeout.readline() - if (re.search(r'(not recognized|No such file|not found)', firstline, - re.IGNORECASE)): - raise IOError('%s must be on your system path.' % cmd) + if re.search( + r"(not recognized|No such file|not found)", firstline, re.IGNORECASE + ): + raise IOError("%s must be on your system path." % cmd) output = firstline + pipeout.read() finally: pipeout.close() return output -if sys.platform == 'win32': - APACHE_PATH = 'httpd' +if sys.platform == "win32": + APACHE_PATH = "httpd" else: - APACHE_PATH = 'apache' + APACHE_PATH = "apache" -CONF_PATH = 'test_mw.conf' +CONF_PATH = "test_mw.conf" conf_modwsgi = r""" # Apache2 server conf file for testing CherryPy with modpython_gateway. @@ -91,7 +92,7 @@ def read_process(cmd, args=''): WSGIScriptAlias / "%(curdir)s/modwsgi.py" SetEnv testmod %(testmod)s -""" # noqa E501 +""" # noqa E501 class ModWSGISupervisor(helper.Supervisor): @@ -103,7 +104,7 @@ class ModWSGISupervisor(helper.Supervisor): def __str__(self): """Render a :class:`ModWSGISupervisor` instance as a string.""" - return 'ModWSGI Server on %s:%s' % (self.host, self.port) + return "ModWSGI Server on %s:%s" % (self.host, self.port) def start(self, modulename): """Spawn an Apache ``mod_wsgi`` supervisor process.""" @@ -111,25 +112,27 @@ def start(self, modulename): if not os.path.isabs(mpconf): mpconf = os.path.join(curdir, mpconf) - with open(mpconf, 'wb') as f: - output = (self.template % - {'port': self.port, 'testmod': modulename, - 'curdir': curdir}) + with open(mpconf, "wb") as f: + output = self.template % { + "port": self.port, + "testmod": modulename, + "curdir": curdir, + } f.write(output) - result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) + result = read_process(APACHE_PATH, "-k start -f %s" % mpconf) if result: print(result) # Make a request so mod_wsgi starts up our app. # If we don't, concurrent initial requests will 404. - portend.occupied('127.0.0.1', self.port, timeout=5) - webtest.openURL('/ihopetheresnodefault', port=self.port) + portend.occupied("127.0.0.1", self.port, timeout=5) + webtest.openURL("/ihopetheresnodefault", port=self.port) time.sleep(1) def stop(self): """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, '-k stop') + read_process(APACHE_PATH, "-k stop") loaded = False @@ -140,15 +143,17 @@ def application(environ, start_response): global loaded if not loaded: loaded = True - modname = 'cherrypy.test.' + environ['testmod'] - mod = __import__(modname, globals(), locals(), ['']) + modname = "cherrypy.test." + environ["testmod"] + mod = __import__(modname, globals(), locals(), [""]) mod.setup_server() - cherrypy.config.update({ - 'log.error_file': os.path.join(curdir, 'test.error.log'), - 'log.access_file': os.path.join(curdir, 'test.access.log'), - 'environment': 'test_suite', - 'engine.SIGHUP': None, - 'engine.SIGTERM': None, - }) + cherrypy.config.update( + { + "log.error_file": os.path.join(curdir, "test.error.log"), + "log.access_file": os.path.join(curdir, "test.access.log"), + "environment": "test_suite", + "engine.SIGHUP": None, + "engine.SIGTERM": None, + } + ) return cherrypy.tree(environ, start_response) diff --git a/cherrypy/test/sessiondemo.py b/cherrypy/test/sessiondemo.py index 989e23a56..06b1ff5db 100755 --- a/cherrypy/test/sessiondemo.py +++ b/cherrypy/test/sessiondemo.py @@ -105,43 +105,40 @@ def page(self): changemsg = [] if cherrypy.session.id != cherrypy.session.originalid: if cherrypy.session.originalid is None: - changemsg.append( - 'Created new session because no session id was given.') + changemsg.append("Created new session because no session id was given.") if cherrypy.session.missing: changemsg.append( - 'Created new session due to missing ' - '(expired or malicious) session.') + "Created new session due to missing " + "(expired or malicious) session." + ) if cherrypy.session.regenerated: - changemsg.append('Application generated a new session.') + changemsg.append("Application generated a new session.") try: - expires = cherrypy.response.cookie['session_id']['expires'] + expires = cherrypy.response.cookie["session_id"]["expires"] except KeyError: - expires = '' + expires = "" return page % { - 'sessionid': cherrypy.session.id, - 'changemsg': '
    '.join(changemsg), - 'respcookie': cherrypy.response.cookie.output(), - 'reqcookie': cherrypy.request.cookie.output(), - 'sessiondata': list(cherrypy.session.items()), - 'servertime': ( - datetime.now(_timezone.utc).strftime('%Y/%m/%d %H:%M UTC') - ), - 'serverunixtime': - calendar.timegm( + "sessionid": cherrypy.session.id, + "changemsg": "
    ".join(changemsg), + "respcookie": cherrypy.response.cookie.output(), + "reqcookie": cherrypy.request.cookie.output(), + "sessiondata": list(cherrypy.session.items()), + "servertime": (datetime.now(_timezone.utc).strftime("%Y/%m/%d %H:%M UTC")), + "serverunixtime": calendar.timegm( datetime.utcnow(_timezone.utc).timetuple(), ), - 'cpversion': cherrypy.__version__, - 'pyversion': sys.version, - 'expires': expires, + "cpversion": cherrypy.__version__, + "pyversion": sys.version, + "expires": expires, } @cherrypy.expose def index(self): """Save green color in session at app index URI.""" # Must modify data or the session will not be saved. - cherrypy.session['color'] = 'green' + cherrypy.session["color"] = "green" return self.page() @cherrypy.expose @@ -155,14 +152,16 @@ def regen(self): """Regenerate the session, storing yellow color in it.""" cherrypy.session.regenerate() # Must modify data or the session will not be saved. - cherrypy.session['color'] = 'yellow' + cherrypy.session["color"] = "yellow" return self.page() -if __name__ == '__main__': - cherrypy.config.update({ - # 'environment': 'production', - 'log.screen': True, - 'tools.sessions.on': True, - }) +if __name__ == "__main__": + cherrypy.config.update( + { + # 'environment': 'production', + "log.screen": True, + "tools.sessions.on": True, + } + ) cherrypy.quickstart(Root()) diff --git a/cherrypy/test/test_auth_basic.py b/cherrypy/test/test_auth_basic.py index f178f8f97..9e3b716de 100644 --- a/cherrypy/test/test_auth_basic.py +++ b/cherrypy/test/test_auth_basic.py @@ -11,39 +11,31 @@ class BasicAuthTest(helper.CPWebCase): - @staticmethod def setup_server(): class Root: - @cherrypy.expose def index(self): - return 'This is public.' + return "This is public." class BasicProtected: - @cherrypy.expose def index(self): - return "Hello %s, you've been authorized." % ( - cherrypy.request.login) + return "Hello %s, you've been authorized." % (cherrypy.request.login) class BasicProtected2: - @cherrypy.expose def index(self): - return "Hello %s, you've been authorized." % ( - cherrypy.request.login) + return "Hello %s, you've been authorized." % (cherrypy.request.login) class BasicProtected2_u: - @cherrypy.expose def index(self): - return "Hello %s, you've been authorized." % ( - cherrypy.request.login) + return "Hello %s, you've been authorized." % (cherrypy.request.login) - userpassdict = {'xuser': 'xpassword'} - userhashdict = {'xuser': md5(b'xpassword').hexdigest()} - userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()} + userpassdict = {"xuser": "xpassword"} + userhashdict = {"xuser": md5(b"xpassword").hexdigest()} + userhashdict_u = {"xюзер": md5(ntob("їжа", "utf-8")).hexdigest()} def checkpasshash(realm, user, password): p = userhashdict.get(user) @@ -51,26 +43,26 @@ def checkpasshash(realm, user, password): def checkpasshash_u(realm, user, password): p = userhashdict_u.get(user) - return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False + return p and p == md5(ntob(password, "utf-8")).hexdigest() or False basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict) conf = { - '/basic': { - 'tools.auth_basic.on': True, - 'tools.auth_basic.realm': 'wonderland', - 'tools.auth_basic.checkpassword': basic_checkpassword_dict + "/basic": { + "tools.auth_basic.on": True, + "tools.auth_basic.realm": "wonderland", + "tools.auth_basic.checkpassword": basic_checkpassword_dict, }, - '/basic2': { - 'tools.auth_basic.on': True, - 'tools.auth_basic.realm': 'wonderland', - 'tools.auth_basic.checkpassword': checkpasshash, - 'tools.auth_basic.accept_charset': 'ISO-8859-1', + "/basic2": { + "tools.auth_basic.on": True, + "tools.auth_basic.realm": "wonderland", + "tools.auth_basic.checkpassword": checkpasshash, + "tools.auth_basic.accept_charset": "ISO-8859-1", }, - '/basic2_u': { - 'tools.auth_basic.on': True, - 'tools.auth_basic.realm': 'wonderland', - 'tools.auth_basic.checkpassword': checkpasshash_u, - 'tools.auth_basic.accept_charset': 'UTF-8', + "/basic2_u": { + "tools.auth_basic.on": True, + "tools.auth_basic.realm": "wonderland", + "tools.auth_basic.checkpassword": checkpasshash_u, + "tools.auth_basic.accept_charset": "UTF-8", }, } @@ -81,55 +73,51 @@ def checkpasshash_u(realm, user, password): cherrypy.tree.mount(root, config=conf) def testPublic(self): - self.getPage('/') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertBody('This is public.') + self.getPage("/") + self.assertStatus("200 OK") + self.assertHeader("Content-Type", "text/html;charset=utf-8") + self.assertBody("This is public.") def testBasic(self): - self.getPage('/basic/') + self.getPage("/basic/") self.assertStatus(401) self.assertHeader( - 'WWW-Authenticate', - 'Basic realm="wonderland", charset="UTF-8"' + "WWW-Authenticate", 'Basic realm="wonderland", charset="UTF-8"' ) - self.getPage('/basic/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.getPage("/basic/", [("Authorization", "Basic eHVzZXI6eHBhc3N3b3JX")]) self.assertStatus(401) - self.getPage('/basic/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) - self.assertStatus('200 OK') + self.getPage("/basic/", [("Authorization", "Basic eHVzZXI6eHBhc3N3b3Jk")]) + self.assertStatus("200 OK") self.assertBody("Hello xuser, you've been authorized.") def testBasic2(self): - self.getPage('/basic2/') + self.getPage("/basic2/") self.assertStatus(401) - self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') + self.assertHeader("WWW-Authenticate", 'Basic realm="wonderland"') - self.getPage('/basic2/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.getPage("/basic2/", [("Authorization", "Basic eHVzZXI6eHBhc3N3b3JX")]) self.assertStatus(401) - self.getPage('/basic2/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) - self.assertStatus('200 OK') + self.getPage("/basic2/", [("Authorization", "Basic eHVzZXI6eHBhc3N3b3Jk")]) + self.assertStatus("200 OK") self.assertBody("Hello xuser, you've been authorized.") def testBasic2_u(self): - self.getPage('/basic2_u/') + self.getPage("/basic2_u/") self.assertStatus(401) self.assertHeader( - 'WWW-Authenticate', - 'Basic realm="wonderland", charset="UTF-8"' + "WWW-Authenticate", 'Basic realm="wonderland", charset="UTF-8"' ) - self.getPage('/basic2_u/', - [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')]) + self.getPage( + "/basic2_u/", [("Authorization", "Basic eNGO0LfQtdGAOtGX0LbRgw==")] + ) self.assertStatus(401) - self.getPage('/basic2_u/', - [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')]) - self.assertStatus('200 OK') + self.getPage( + "/basic2_u/", [("Authorization", "Basic eNGO0LfQtdGAOtGX0LbQsA==")] + ) + self.assertStatus("200 OK") self.assertBody("Hello xюзер, you've been authorized.") diff --git a/cherrypy/test/test_auth_digest.py b/cherrypy/test/test_auth_digest.py index 4b7b5298f..94b936814 100644 --- a/cherrypy/test/test_auth_digest.py +++ b/cherrypy/test/test_auth_digest.py @@ -11,121 +11,134 @@ def _fetch_users(): - return {'test': 'test', '☃йюзер': 'їпароль'} + return {"test": "test", "☃йюзер": "їпароль"} get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users()) class DigestAuthTest(helper.CPWebCase): - @staticmethod def setup_server(): class Root: - @cherrypy.expose def index(self): - return 'This is public.' + return "This is public." class DigestProtected: - @cherrypy.expose def index(self, *args, **kwargs): - return "Hello %s, you've been authorized." % ( - cherrypy.request.login) - - conf = {'/digest': {'tools.auth_digest.on': True, - 'tools.auth_digest.realm': 'localhost', - 'tools.auth_digest.get_ha1': get_ha1, - 'tools.auth_digest.key': 'a565c27146791cfb', - 'tools.auth_digest.debug': True, - 'tools.auth_digest.accept_charset': 'UTF-8'}} + return "Hello %s, you've been authorized." % (cherrypy.request.login) + + conf = { + "/digest": { + "tools.auth_digest.on": True, + "tools.auth_digest.realm": "localhost", + "tools.auth_digest.get_ha1": get_ha1, + "tools.auth_digest.key": "a565c27146791cfb", + "tools.auth_digest.debug": True, + "tools.auth_digest.accept_charset": "UTF-8", + } + } root = Root() root.digest = DigestProtected() cherrypy.tree.mount(root, config=conf) def testPublic(self): - self.getPage('/') - assert self.status == '200 OK' - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - assert self.body == b'This is public.' + self.getPage("/") + assert self.status == "200 OK" + self.assertHeader("Content-Type", "text/html;charset=utf-8") + assert self.body == b"This is public." def _test_parametric_digest(self, username, realm): - test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path' + test_uri = "/digest/?@/=%2F%40&%f0%9f%99%88=path" self.getPage(test_uri) assert self.status_code == 401 - msg = 'Digest authentification scheme was not found' - www_auth_digest = tuple(filter( - lambda kv: kv[0].lower() == 'www-authenticate' - and kv[1].startswith('Digest '), - self.headers, - )) + msg = "Digest authentification scheme was not found" + www_auth_digest = tuple( + filter( + lambda kv: kv[0].lower() == "www-authenticate" + and kv[1].startswith("Digest "), + self.headers, + ) + ) assert len(www_auth_digest) == 1, msg - items = www_auth_digest[0][-1][7:].split(', ') + items = www_auth_digest[0][-1][7:].split(", ") tokens = {} for item in items: - key, value = item.split('=') + key, value = item.split("=") tokens[key.lower()] = value - assert tokens['realm'] == '"localhost"' - assert tokens['algorithm'] == '"MD5"' - assert tokens['qop'] == '"auth"' - assert tokens['charset'] == '"UTF-8"' + assert tokens["realm"] == '"localhost"' + assert tokens["algorithm"] == '"MD5"' + assert tokens["qop"] == '"auth"' + assert tokens["charset"] == '"UTF-8"' - nonce = tokens['nonce'].strip('"') + nonce = tokens["nonce"].strip('"') # Test user agent response with a wrong value for 'realm' - base_auth = ('Digest username="%s", ' - 'realm="%s", ' - 'nonce="%s", ' - 'uri="%s", ' - 'algorithm=MD5, ' - 'response="%s", ' - 'qop=auth, ' - 'nc=%s, ' - 'cnonce="1522e61005789929"') + base_auth = ( + 'Digest username="%s", ' + 'realm="%s", ' + 'nonce="%s", ' + 'uri="%s", ' + "algorithm=MD5, " + 'response="%s", ' + "qop=auth, " + "nc=%s, " + 'cnonce="1522e61005789929"' + ) encoded_user = username - encoded_user = encoded_user.encode('utf-8') - encoded_user = encoded_user.decode('latin1') + encoded_user = encoded_user.encode("utf-8") + encoded_user = encoded_user.decode("latin1") auth_header = base_auth % ( - encoded_user, realm, nonce, test_uri, - '11111111111111111111111111111111', '00000001', + encoded_user, + realm, + nonce, + test_uri, + "11111111111111111111111111111111", + "00000001", ) - auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') + auth = auth_digest.HttpDigestAuthorization(auth_header, "GET") # calculate the response digest ha1 = get_ha1(auth.realm, auth.username) response = auth.request_digest(ha1) auth_header = base_auth % ( - encoded_user, realm, nonce, test_uri, - response, '00000001', + encoded_user, + realm, + nonce, + test_uri, + response, + "00000001", ) - self.getPage(test_uri, [('Authorization', auth_header)]) + self.getPage(test_uri, [("Authorization", auth_header)]) def test_wrong_realm(self): # send response with correct response digest, but wrong realm - self._test_parametric_digest(username='test', realm='wrong realm') + self._test_parametric_digest(username="test", realm="wrong realm") assert self.status_code == 401 def test_ascii_user(self): - self._test_parametric_digest(username='test', realm='localhost') - assert self.status == '200 OK' + self._test_parametric_digest(username="test", realm="localhost") + assert self.status == "200 OK" assert self.body == b"Hello test, you've been authorized." def test_unicode_user(self): - self._test_parametric_digest(username='☃йюзер', realm='localhost') - assert self.status == '200 OK' + self._test_parametric_digest(username="☃йюзер", realm="localhost") + assert self.status == "200 OK" assert self.body == ntob( - "Hello ☃йюзер, you've been authorized.", 'utf-8', + "Hello ☃йюзер, you've been authorized.", + "utf-8", ) def test_wrong_scheme(self): basic_auth = { - 'Authorization': 'Basic foo:bar', + "Authorization": "Basic foo:bar", } - self.getPage('/digest/', headers=list(basic_auth.items())) + self.getPage("/digest/", headers=list(basic_auth.items())) assert self.status_code == 401 diff --git a/cherrypy/test/test_bus.py b/cherrypy/test/test_bus.py index 594023a23..323827edb 100644 --- a/cherrypy/test/test_bus.py +++ b/cherrypy/test/test_bus.py @@ -12,8 +12,8 @@ from cherrypy.process import wspbus -CI_ON_MACOS = bool(os.getenv('CI')) and sys.platform == 'darwin' -msg = 'Listener %d on channel %s: %s.' # pylint: disable=invalid-name +CI_ON_MACOS = bool(os.getenv("CI")) and sys.platform == "darwin" +msg = "Listener %d on channel %s: %s." # pylint: disable=invalid-name @pytest.fixture @@ -25,6 +25,7 @@ def bus(): @pytest.fixture def log_tracker(bus): """Return an instance of bus log tracker.""" + class LogTracker: # pylint: disable=too-few-public-methods """Bus log tracker.""" @@ -33,7 +34,8 @@ class LogTracker: # pylint: disable=too-few-public-methods def __init__(self, bus): def logit(msg, level): # pylint: disable=unused-argument self.log_entries.append(msg) - bus.subscribe('log', logit) + + bus.subscribe("log", logit) return LogTracker(bus) @@ -41,6 +43,7 @@ def logit(msg, level): # pylint: disable=unused-argument @pytest.fixture def listener(): """Return an instance of bus response tracker.""" + class Listner: # pylint: disable=too-few-public-methods """Bus handler return value tracker.""" @@ -48,8 +51,10 @@ class Listner: # pylint: disable=too-few-public-methods def get_listener(self, channel, index): """Return an argument tracking listener.""" + def listener(arg=None): self.responses.append(msg % (index, channel, arg)) + return listener return Listner() @@ -80,7 +85,7 @@ def test_custom_channels(bus, listener): """Test that custom pub-sub channels work as built-in ones.""" expected = [] - custom_listeners = ('hugh', 'louis', 'dewey') + custom_listeners = ("hugh", "louis", "dewey") for channel in custom_listeners: for index, priority in enumerate([None, 10, 60, 40]): bus.subscribe( @@ -90,8 +95,8 @@ def test_custom_channels(bus, listener): ) for channel in custom_listeners: - bus.publish(channel, 'ah so') - expected.extend(msg % (i, channel, 'ah so') for i in (1, 3, 0, 2)) + bus.publish(channel, "ah so") + expected.extend(msg % (i, channel, "ah so") for i in (1, 3, 0, 2)) bus.publish(channel) expected.extend(msg % (i, channel, None) for i in (1, 3, 0, 2)) @@ -101,7 +106,7 @@ def test_custom_channels(bus, listener): def test_listener_errors(bus, listener): """Test that unhandled exceptions raise channel failures.""" expected = [] - channels = [c for c in bus.listeners if c != 'log'] + channels = [c for c in bus.listeners if c != "log"] for channel in channels: bus.subscribe(channel, listener.get_listener(channel, 1)) @@ -120,19 +125,19 @@ def test_start(bus, listener, log_tracker): """Test that bus start sequence calls all listeners.""" num = 3 for index in range(num): - bus.subscribe('start', listener.get_listener('start', index)) + bus.subscribe("start", listener.get_listener("start", index)) bus.start() try: # The start method MUST call all 'start' listeners. - assert ( - set(listener.responses) == - set(msg % (i, 'start', None) for i in range(num))) + assert set(listener.responses) == set( + msg % (i, "start", None) for i in range(num) + ) # The start method MUST move the state to STARTED # (or EXITING, if errors occur) assert bus.state == bus.states.STARTED # The start method MUST log its states. - assert log_tracker.log_entries == ['Bus STARTING', 'Bus STARTED'] + assert log_tracker.log_entries == ["Bus STARTING", "Bus STARTED"] finally: # Exit so the atexit handler doesn't complain. bus.exit() @@ -143,19 +148,18 @@ def test_stop(bus, listener, log_tracker): num = 3 for index in range(num): - bus.subscribe('stop', listener.get_listener('stop', index)) + bus.subscribe("stop", listener.get_listener("stop", index)) bus.stop() # The stop method MUST call all 'stop' listeners. - assert (set(listener.responses) == - set(msg % (i, 'stop', None) for i in range(num))) + assert set(listener.responses) == set(msg % (i, "stop", None) for i in range(num)) # The stop method MUST move the state to STOPPED assert bus.state == bus.states.STOPPED # The stop method MUST log its states. - assert log_tracker.log_entries == ['Bus STOPPING', 'Bus STOPPED'] + assert log_tracker.log_entries == ["Bus STOPPING", "Bus STOPPED"] def test_graceful(bus, listener, log_tracker): @@ -163,17 +167,17 @@ def test_graceful(bus, listener, log_tracker): num = 3 for index in range(num): - bus.subscribe('graceful', listener.get_listener('graceful', index)) + bus.subscribe("graceful", listener.get_listener("graceful", index)) bus.graceful() # The graceful method MUST call all 'graceful' listeners. - assert ( - set(listener.responses) == - set(msg % (i, 'graceful', None) for i in range(num))) + assert set(listener.responses) == set( + msg % (i, "graceful", None) for i in range(num) + ) # The graceful method MUST log its states. - assert log_tracker.log_entries == ['Bus graceful'] + assert log_tracker.log_entries == ["Bus graceful"] def test_exit(bus, listener, log_tracker): @@ -181,36 +185,42 @@ def test_exit(bus, listener, log_tracker): num = 3 for index in range(num): - bus.subscribe('stop', listener.get_listener('stop', index)) - bus.subscribe('exit', listener.get_listener('exit', index)) + bus.subscribe("stop", listener.get_listener("stop", index)) + bus.subscribe("exit", listener.get_listener("exit", index)) bus.exit() # The exit method MUST call all 'stop' listeners, # and then all 'exit' listeners. - assert (set(listener.responses) == - set([msg % (i, 'stop', None) for i in range(num)] + - [msg % (i, 'exit', None) for i in range(num)])) + assert set(listener.responses) == set( + [msg % (i, "stop", None) for i in range(num)] + + [msg % (i, "exit", None) for i in range(num)] + ) # The exit method MUST move the state to EXITING assert bus.state == bus.states.EXITING # The exit method MUST log its states. - assert (log_tracker.log_entries == - ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) + assert log_tracker.log_entries == [ + "Bus STOPPING", + "Bus STOPPED", + "Bus EXITING", + "Bus EXITED", + ] def test_wait(bus): """Test that bus wait awaits for states.""" + def f(method): # pylint: disable=invalid-name time.sleep(0.2) getattr(bus, method)() flow = [ - ('start', [bus.states.STARTED]), - ('stop', [bus.states.STOPPED]), - ('start', [bus.states.STARTING, bus.states.STARTED]), - ('exit', [bus.states.EXITING]), + ("start", [bus.states.STARTED]), + ("stop", [bus.states.STOPPED]), + ("start", [bus.states.STARTING, bus.states.STARTED]), + ("exit", [bus.states.EXITING]), ] for method, states in flow: @@ -218,25 +228,27 @@ def f(method): # pylint: disable=invalid-name bus.wait(states) # The wait method MUST wait for the given state(s). - assert bus.state in states, 'State %r not in %r' % (bus.state, states) + assert bus.state in states, "State %r not in %r" % (bus.state, states) -@pytest.mark.xfail(CI_ON_MACOS, reason='continuous integration on macOS fails') +@pytest.mark.xfail(CI_ON_MACOS, reason="continuous integration on macOS fails") def test_wait_publishes_periodically(bus): """Test that wait publishes each tick.""" callback = unittest.mock.MagicMock() - bus.subscribe('main', callback) + 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') + bus.wait(bus.states.STARTED, interval=0.01, channel="main") assert callback.call_count > 3 def test_block(bus, log_tracker): """Test that bus block waits for exiting.""" + def f(): # pylint: disable=invalid-name time.sleep(0.2) bus.exit() @@ -262,19 +274,19 @@ def g(): # pylint: disable=invalid-name # The last message will mention an indeterminable thread name; ignore # it expected_bus_messages = [ - 'Bus STOPPING', - 'Bus STOPPED', - 'Bus EXITING', - 'Bus EXITED', - 'Waiting for child threads to terminate...', + "Bus STOPPING", + "Bus STOPPED", + "Bus EXITING", + "Bus EXITED", + "Waiting for child threads to terminate...", ] bus_msg_num = len(expected_bus_messages) # If the last message mentions an indeterminable thread name then ignore it assert log_tracker.log_entries[:bus_msg_num] == expected_bus_messages - assert len(log_tracker.log_entries[bus_msg_num:]) <= 1, ( - 'No more than one extra log line with the thread name expected' - ) + assert ( + len(log_tracker.log_entries[bus_msg_num:]) <= 1 + ), "No more than one extra log line with the thread name expected" def test_start_with_callback(bus): @@ -283,12 +295,13 @@ def test_start_with_callback(bus): events = [] def f(*args, **kwargs): # pylint: disable=invalid-name - events.append(('f', args, kwargs)) + events.append(("f", args, kwargs)) def g(): # pylint: disable=invalid-name - events.append('g') - bus.subscribe('start', g) - bus.start_with_callback(f, (1, 3, 5), {'foo': 'bar'}) + events.append("g") + + bus.subscribe("start", g) + bus.start_with_callback(f, (1, 3, 5), {"foo": "bar"}) # Give wait() time to run f() time.sleep(0.2) @@ -297,7 +310,7 @@ def g(): # pylint: disable=invalid-name assert bus.state == bus.states.STARTED # The callback method MUST run after all start methods. - assert events == ['g', ('f', (1, 3, 5), {'foo': 'bar'})] + assert events == ["g", ("f", (1, 3, 5), {"foo": "bar"})] finally: bus.exit() @@ -308,7 +321,7 @@ def test_log(bus, log_tracker): # Try a normal message. expected = [] - for msg_ in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']: + for msg_ in ["O mah darlin'"] * 3 + ["Clementiiiiiiiine"]: bus.log(msg_) expected.append(msg_) assert log_tracker.log_entries == expected @@ -317,11 +330,10 @@ def test_log(bus, log_tracker): try: foo except NameError: - bus.log('You are lost and gone forever', traceback=True) + bus.log("You are lost and gone forever", traceback=True) lastmsg = log_tracker.log_entries[-1] - assert 'Traceback' in lastmsg and 'NameError' in lastmsg, ( - 'Last log message %r did not contain ' - 'the expected traceback.' % lastmsg + assert "Traceback" in lastmsg and "NameError" in lastmsg, ( + "Last log message %r did not contain " "the expected traceback." % lastmsg ) else: - pytest.fail('NameError was not raised as expected.') + pytest.fail("NameError was not raised as expected.") diff --git a/cherrypy/test/test_caching.py b/cherrypy/test/test_caching.py index c0b897976..439102fef 100644 --- a/cherrypy/test/test_caching.py +++ b/cherrypy/test/test_caching.py @@ -17,19 +17,16 @@ gif_bytes = ( b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;' + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;" ) class CacheTest(helper.CPWebCase): - @staticmethod def setup_server(): - - @cherrypy.config(**{'tools.caching.on': True}) + @cherrypy.config(**{"tools.caching.on": True}) class Root: - def __init__(self): self.counter = 0 self.control_counter = 0 @@ -38,317 +35,312 @@ def __init__(self): @cherrypy.expose def index(self): self.counter += 1 - msg = 'visit #%s' % self.counter + msg = "visit #%s" % self.counter return msg @cherrypy.expose def control(self): self.control_counter += 1 - return 'visit #%s' % self.control_counter + return "visit #%s" % self.control_counter @cherrypy.expose def a_gif(self): - cherrypy.response.headers[ - 'Last-Modified'] = httputil.HTTPDate() + cherrypy.response.headers["Last-Modified"] = httputil.HTTPDate() return gif_bytes @cherrypy.expose - def long_process(self, seconds='1'): + def long_process(self, seconds="1"): try: self.longlock.acquire() time.sleep(float(seconds)) finally: self.longlock.release() - return 'success!' + return "success!" @cherrypy.expose def clear_cache(self, path): cherrypy._cache.store[cherrypy.request.base + path].clear() - @cherrypy.config(**{ - 'tools.caching.on': True, - 'tools.response_headers.on': True, - 'tools.response_headers.headers': [ - ('Vary', 'Our-Varying-Header') - ], - }) + @cherrypy.config( + **{ + "tools.caching.on": True, + "tools.response_headers.on": True, + "tools.response_headers.headers": [("Vary", "Our-Varying-Header")], + } + ) class VaryHeaderCachingServer(object): - def __init__(self): self.counter = count(1) @cherrypy.expose def index(self): - return 'visit #%s' % next(self.counter) - - @cherrypy.config(**{ - 'tools.expires.on': True, - 'tools.expires.secs': 60, - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.root': curdir, - }) + return "visit #%s" % next(self.counter) + + @cherrypy.config( + **{ + "tools.expires.on": True, + "tools.expires.secs": 60, + "tools.staticdir.on": True, + "tools.staticdir.dir": "static", + "tools.staticdir.root": curdir, + } + ) class UnCached(object): - @cherrypy.expose - @cherrypy.config(**{'tools.expires.secs': 0}) + @cherrypy.config(**{"tools.expires.secs": 0}) def force(self): - cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' - self._cp_config['tools.expires.force'] = True - self._cp_config['tools.expires.secs'] = 0 - return 'being forceful' + cherrypy.response.headers["Etag"] = "bibbitybobbityboo" + self._cp_config["tools.expires.force"] = True + self._cp_config["tools.expires.secs"] = 0 + return "being forceful" @cherrypy.expose def dynamic(self): - cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' - cherrypy.response.headers['Cache-Control'] = 'private' - return 'D-d-d-dynamic!' + cherrypy.response.headers["Etag"] = "bibbitybobbityboo" + cherrypy.response.headers["Cache-Control"] = "private" + return "D-d-d-dynamic!" @cherrypy.expose def cacheable(self): - cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + cherrypy.response.headers["Etag"] = "bibbitybobbityboo" return "Hi, I'm cacheable." @cherrypy.expose - @cherrypy.config(**{'tools.expires.secs': 86400}) + @cherrypy.config(**{"tools.expires.secs": 86400}) def specific(self): - cherrypy.response.headers[ - 'Etag'] = 'need_this_to_make_me_cacheable' - return 'I am being specific' + cherrypy.response.headers["Etag"] = "need_this_to_make_me_cacheable" + return "I am being specific" class Foo(object): pass @cherrypy.expose - @cherrypy.config(**{'tools.expires.secs': Foo()}) + @cherrypy.config(**{"tools.expires.secs": Foo()}) def wrongtype(self): - cherrypy.response.headers[ - 'Etag'] = 'need_this_to_make_me_cacheable' - return 'Woops' - - @cherrypy.config(**{ - 'tools.gzip.mime_types': ['text/*', 'image/*'], - 'tools.caching.on': True, - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.root': curdir - }) + cherrypy.response.headers["Etag"] = "need_this_to_make_me_cacheable" + return "Woops" + + @cherrypy.config( + **{ + "tools.gzip.mime_types": ["text/*", "image/*"], + "tools.caching.on": True, + "tools.staticdir.on": True, + "tools.staticdir.dir": "static", + "tools.staticdir.root": curdir, + } + ) class GzipStaticCache(object): pass cherrypy.tree.mount(Root()) - cherrypy.tree.mount(UnCached(), '/expires') - cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers') - cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache') - cherrypy.config.update({'tools.gzip.on': True}) + cherrypy.tree.mount(UnCached(), "/expires") + cherrypy.tree.mount(VaryHeaderCachingServer(), "/varying_headers") + cherrypy.tree.mount(GzipStaticCache(), "/gzip_static_cache") + cherrypy.config.update({"tools.gzip.on": True}) def testCaching(self): elapsed = 0.0 for trial in range(10): - self.getPage('/') + self.getPage("/") # The response should be the same every time, # except for the Age response header. - self.assertBody('visit #1') + self.assertBody("visit #1") if trial != 0: - age = int(self.assertHeader('Age')) + age = int(self.assertHeader("Age")) assert age >= elapsed elapsed = age # POST, PUT, DELETE should not be cached. - self.getPage('/', method='POST') - self.assertBody('visit #2') + self.getPage("/", method="POST") + self.assertBody("visit #2") # Because gzip is turned on, the Vary header should always Vary for # content-encoding - self.assertHeader('Vary', 'Accept-Encoding') + self.assertHeader("Vary", "Accept-Encoding") # The previous request should have invalidated the cache, # so this request will recalc the response. - self.getPage('/', method='GET') - self.assertBody('visit #3') + self.getPage("/", method="GET") + self.assertBody("visit #3") # ...but this request should get the cached copy. - self.getPage('/', method='GET') - self.assertBody('visit #3') - self.getPage('/', method='DELETE') - self.assertBody('visit #4') + self.getPage("/", method="GET") + self.assertBody("visit #3") + self.getPage("/", method="DELETE") + self.assertBody("visit #4") # The previous request should have invalidated the cache, # so this request will recalc the response. - self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) - self.assertHeader('Content-Encoding', 'gzip') - self.assertHeader('Vary') - self.assertEqual( - cherrypy.lib.encoding.decompress(self.body), b'visit #5') + self.getPage("/", method="GET", headers=[("Accept-Encoding", "gzip")]) + self.assertHeader("Content-Encoding", "gzip") + self.assertHeader("Vary") + self.assertEqual(cherrypy.lib.encoding.decompress(self.body), b"visit #5") # Now check that a second request gets the gzip header and gzipped body # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped # response body was being gzipped a second time. - self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) - self.assertHeader('Content-Encoding', 'gzip') - self.assertEqual( - cherrypy.lib.encoding.decompress(self.body), b'visit #5') + self.getPage("/", method="GET", headers=[("Accept-Encoding", "gzip")]) + self.assertHeader("Content-Encoding", "gzip") + self.assertEqual(cherrypy.lib.encoding.decompress(self.body), b"visit #5") # Now check that a third request that doesn't accept gzip # skips the cache (because the 'Vary' header denies it). - self.getPage('/', method='GET') - self.assertNoHeader('Content-Encoding') - self.assertBody('visit #6') + self.getPage("/", method="GET") + self.assertNoHeader("Content-Encoding") + self.assertBody("visit #6") def testVaryHeader(self): - self.getPage('/varying_headers/') - self.assertStatus('200 OK') - self.assertHeaderItemValue('Vary', 'Our-Varying-Header') - self.assertBody('visit #1') + self.getPage("/varying_headers/") + self.assertStatus("200 OK") + self.assertHeaderItemValue("Vary", "Our-Varying-Header") + self.assertBody("visit #1") # Now check that different 'Vary'-fields don't evict each other. # This test creates 2 requests with different 'Our-Varying-Header' # and then tests if the first one still exists. - self.getPage('/varying_headers/', - headers=[('Our-Varying-Header', 'request 2')]) - self.assertStatus('200 OK') - self.assertBody('visit #2') + self.getPage("/varying_headers/", headers=[("Our-Varying-Header", "request 2")]) + self.assertStatus("200 OK") + self.assertBody("visit #2") - self.getPage('/varying_headers/', - headers=[('Our-Varying-Header', 'request 2')]) - self.assertStatus('200 OK') - self.assertBody('visit #2') + self.getPage("/varying_headers/", headers=[("Our-Varying-Header", "request 2")]) + self.assertStatus("200 OK") + self.assertBody("visit #2") - self.getPage('/varying_headers/') - self.assertStatus('200 OK') - self.assertBody('visit #1') + self.getPage("/varying_headers/") + self.assertStatus("200 OK") + self.assertBody("visit #1") def testExpiresTool(self): # test setting an expires header - self.getPage('/expires/specific') - self.assertStatus('200 OK') - self.assertHeader('Expires') + self.getPage("/expires/specific") + self.assertStatus("200 OK") + self.assertHeader("Expires") # test exceptions for bad time values - self.getPage('/expires/wrongtype') + self.getPage("/expires/wrongtype") self.assertStatus(500) - self.assertInBody('TypeError') + self.assertInBody("TypeError") # static content should not have "cache prevention" headers - self.getPage('/expires/index.html') - self.assertStatus('200 OK') - self.assertNoHeader('Pragma') - self.assertNoHeader('Cache-Control') - self.assertHeader('Expires') + self.getPage("/expires/index.html") + self.assertStatus("200 OK") + self.assertNoHeader("Pragma") + self.assertNoHeader("Cache-Control") + self.assertHeader("Expires") # dynamic content that sets indicators should not have # "cache prevention" headers - self.getPage('/expires/cacheable') - self.assertStatus('200 OK') - self.assertNoHeader('Pragma') - self.assertNoHeader('Cache-Control') - self.assertHeader('Expires') - - self.getPage('/expires/dynamic') - self.assertBody('D-d-d-dynamic!') + self.getPage("/expires/cacheable") + self.assertStatus("200 OK") + self.assertNoHeader("Pragma") + self.assertNoHeader("Cache-Control") + self.assertHeader("Expires") + + self.getPage("/expires/dynamic") + self.assertBody("D-d-d-dynamic!") # the Cache-Control header should be untouched - self.assertHeader('Cache-Control', 'private') - self.assertHeader('Expires') + self.assertHeader("Cache-Control", "private") + self.assertHeader("Expires") # configure the tool to ignore indicators and replace existing headers - self.getPage('/expires/force') - self.assertStatus('200 OK') + self.getPage("/expires/force") + self.assertStatus("200 OK") # This also gives us a chance to test 0 expiry with no other headers - self.assertHeader('Pragma', 'no-cache') - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.assertHeader('Cache-Control', 'no-cache, must-revalidate') - self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") # static content should now have "cache prevention" headers - self.getPage('/expires/index.html') - self.assertStatus('200 OK') - self.assertHeader('Pragma', 'no-cache') - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.assertHeader('Cache-Control', 'no-cache, must-revalidate') - self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + self.getPage("/expires/index.html") + self.assertStatus("200 OK") + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") # the cacheable handler should now have "cache prevention" headers - self.getPage('/expires/cacheable') - self.assertStatus('200 OK') - self.assertHeader('Pragma', 'no-cache') - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.assertHeader('Cache-Control', 'no-cache, must-revalidate') - self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') - - self.getPage('/expires/dynamic') - self.assertBody('D-d-d-dynamic!') + self.getPage("/expires/cacheable") + self.assertStatus("200 OK") + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") + + self.getPage("/expires/dynamic") + self.assertBody("D-d-d-dynamic!") # dynamic sets Cache-Control to private but it should be # overwritten here ... - self.assertHeader('Pragma', 'no-cache') - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.assertHeader('Cache-Control', 'no-cache, must-revalidate') - self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") def _assert_resp_len_and_enc_for_gzip(self, uri): """ Test that after querying gzipped content it's remains valid in cache and available non-gzipped as well. """ - ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')] + ACCEPT_GZIP_HEADERS = [("Accept-Encoding", "gzip")] content_len = None for _ in range(3): - self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS) + self.getPage(uri, method="GET", headers=ACCEPT_GZIP_HEADERS) if content_len is not None: # all requests should get the same length - self.assertHeader('Content-Length', content_len) - self.assertHeader('Content-Encoding', 'gzip') + self.assertHeader("Content-Length", content_len) + self.assertHeader("Content-Encoding", "gzip") - content_len = dict(self.headers)['Content-Length'] + content_len = dict(self.headers)["Content-Length"] # check that we can still get non-gzipped version - self.getPage(uri, method='GET') - self.assertNoHeader('Content-Encoding') + self.getPage(uri, method="GET") + self.assertNoHeader("Content-Encoding") # non-gzipped version should have a different content length - self.assertNoHeaderItemValue('Content-Length', content_len) + self.assertNoHeaderItemValue("Content-Length", content_len) def testGzipStaticCache(self): """Test that cache and gzip tools play well together when both enabled. Ref GitHub issue #1190. """ - GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}' - resource_files = ('index.html', 'dirback.jpg') + GZIP_STATIC_CACHE_TMPL = "/gzip_static_cache/{}" + resource_files = ("index.html", "dirback.jpg") for f in resource_files: uri = GZIP_STATIC_CACHE_TMPL.format(f) self._assert_resp_len_and_enc_for_gzip(uri) def testLastModified(self): - self.getPage('/a.gif') + self.getPage("/a.gif") self.assertStatus(200) self.assertBody(gif_bytes) - lm1 = self.assertHeader('Last-Modified') + lm1 = self.assertHeader("Last-Modified") # this request should get the cached copy. - self.getPage('/a.gif') + self.getPage("/a.gif") self.assertStatus(200) self.assertBody(gif_bytes) - self.assertHeader('Age') - lm2 = self.assertHeader('Last-Modified') + self.assertHeader("Age") + lm2 = self.assertHeader("Last-Modified") self.assertEqual(lm1, lm2) # this request should match the cached copy, but raise 304. - self.getPage('/a.gif', [('If-Modified-Since', lm1)]) + self.getPage("/a.gif", [("If-Modified-Since", lm1)]) self.assertStatus(304) - self.assertNoHeader('Last-Modified') - if not getattr(cherrypy.server, 'using_apache', False): - self.assertHeader('Age') + self.assertNoHeader("Last-Modified") + if not getattr(cherrypy.server, "using_apache", False): + self.assertHeader("Age") - @pytest.mark.xfail(reason='#1536') + @pytest.mark.xfail(reason="#1536") def test_antistampede(self): SECONDS = 4 - slow_url = '/long_process?seconds={SECONDS}'.format(**locals()) + slow_url = "/long_process?seconds={SECONDS}".format(**locals()) # We MUST make an initial synchronous request in order to create the # AntiStampedeCache object, and populate its selecting_headers, # before the actual stampede. self.getPage(slow_url) - self.assertBody('success!') - path = urllib.parse.quote(slow_url, safe='') - self.getPage('/clear_cache?path=' + path) + self.assertBody("success!") + path = urllib.parse.quote(slow_url, safe="") + self.getPage("/clear_cache?path=" + path) self.assertStatus(200) start = datetime.datetime.now() @@ -356,7 +348,8 @@ def test_antistampede(self): def run(): self.getPage(slow_url) # The response should be the same every time - self.assertBody('success!') + self.assertBody("success!") + ts = [threading.Thread(target=run) for i in range(100)] for t in ts: t.start() @@ -368,23 +361,23 @@ def run(): self.assertEqualDates(start, finish, seconds=allowance) def test_cache_control(self): - self.getPage('/control') - self.assertBody('visit #1') - self.getPage('/control') - self.assertBody('visit #1') + self.getPage("/control") + self.assertBody("visit #1") + self.getPage("/control") + self.assertBody("visit #1") - self.getPage('/control', headers=[('Cache-Control', 'no-cache')]) - self.assertBody('visit #2') - self.getPage('/control') - self.assertBody('visit #2') + self.getPage("/control", headers=[("Cache-Control", "no-cache")]) + self.assertBody("visit #2") + self.getPage("/control") + self.assertBody("visit #2") - self.getPage('/control', headers=[('Pragma', 'no-cache')]) - self.assertBody('visit #3') - self.getPage('/control') - self.assertBody('visit #3') + self.getPage("/control", headers=[("Pragma", "no-cache")]) + self.assertBody("visit #3") + self.getPage("/control") + self.assertBody("visit #3") time.sleep(1) - self.getPage('/control', headers=[('Cache-Control', 'max-age=0')]) - self.assertBody('visit #4') - self.getPage('/control') - self.assertBody('visit #4') + self.getPage("/control", headers=[("Cache-Control", "max-age=0")]) + self.assertBody("visit #4") + self.getPage("/control") + self.assertBody("visit #4") diff --git a/cherrypy/test/test_config.py b/cherrypy/test/test_config.py index ecd460198..878fb3a62 100644 --- a/cherrypy/test/test_config.py +++ b/cherrypy/test/test_config.py @@ -18,20 +18,18 @@ def StringIOFromNative(x): def setup_server(): - - @cherrypy.config(foo='this', bar='that') + @cherrypy.config(foo="this", bar="that") class Root: - def __init__(self): - cherrypy.config.namespaces['db'] = self.db_namespace + cherrypy.config.namespaces["db"] = self.db_namespace def db_namespace(self, k, v): - if k == 'scheme': + if k == "scheme": self.db = v - @cherrypy.expose(alias=('global_', 'xyz')) + @cherrypy.expose(alias=("global_", "xyz")) def index(self, key): - return cherrypy.request.config.get(key, 'None') + return cherrypy.request.config.get(key, "None") @cherrypy.expose def repr(self, key): @@ -42,40 +40,40 @@ def dbscheme(self): return self.db @cherrypy.expose - @cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']}) + @cherrypy.config(**{"request.body.attempt_charsets": ["utf-16"]}) def plain(self, x): return x favicon_ico = cherrypy.tools.staticfile.handler( - filename=os.path.join(localDir, '../favicon.ico')) + filename=os.path.join(localDir, "../favicon.ico") + ) - @cherrypy.config(foo='this2', baz='that2') + @cherrypy.config(foo="this2", baz="that2") class Foo: - @cherrypy.expose def index(self, key): - return cherrypy.request.config.get(key, 'None') + return cherrypy.request.config.get(key, "None") + nex = index @cherrypy.expose - @cherrypy.config(**{'response.headers.X-silly': 'sillyval'}) + @cherrypy.config(**{"response.headers.X-silly": "sillyval"}) def silly(self): - return 'Hello world' + return "Hello world" # Test the expose and config decorators - @cherrypy.config(foo='this3', **{'bax': 'this4'}) + @cherrypy.config(foo="this3", **{"bax": "this4"}) @cherrypy.expose def bar(self, key): return repr(cherrypy.request.config.get(key, None)) class Another: - @cherrypy.expose def index(self, key): - return str(cherrypy.request.config.get(key, 'None')) + return str(cherrypy.request.config.get(key, "None")) def raw_namespace(key, value): - if key == 'input.map': + if key == "input.map": handler = cherrypy.request.handler def wrapper(): @@ -86,24 +84,26 @@ def wrapper(): except KeyError: pass return handler() + cherrypy.request.handler = wrapper - elif key == 'output': + elif key == "output": handler = cherrypy.request.handler def wrapper(): # 'value' is a type (like int or str). return value(handler()) + cherrypy.request.handler = wrapper - @cherrypy.config(**{'raw.output': repr}) + @cherrypy.config(**{"raw.output": repr}) class Raw: - @cherrypy.expose - @cherrypy.config(**{'raw.input.map': {'num': int}}) + @cherrypy.config(**{"raw.input.map": {"num": int}}) def incr(self, num): return num + 1 - ioconf = StringIOFromNative(""" + ioconf = StringIOFromNative( + """ [/] neg: -1234 filename: os.path.join(sys.prefix, "hello.py") @@ -117,18 +117,23 @@ def incr(self, num): [/favicon.ico] tools.staticfile.filename = %r -""" % os.path.join(localDir, 'static/dirback.jpg')) +""" + % os.path.join(localDir, "static/dirback.jpg") + ) root = Root() root.foo = Foo() root.raw = Raw() app = cherrypy.tree.mount(root, config=ioconf) - app.request_class.namespaces['raw'] = raw_namespace + app.request_class.namespaces["raw"] = raw_namespace - cherrypy.tree.mount(Another(), '/another') - cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', - 'db.scheme': r'sqlite///memory', - }) + cherrypy.tree.mount(Another(), "/another") + cherrypy.config.update( + { + "luxuryyacht": "throatwobblermangrove", + "db.scheme": r"sqlite///memory", + } + ) # Client-side code # @@ -139,97 +144,103 @@ class ConfigTests(helper.CPWebCase): def testConfig(self): tests = [ - ('/', 'nex', 'None'), - ('/', 'foo', 'this'), - ('/', 'bar', 'that'), - ('/xyz', 'foo', 'this'), - ('/foo/', 'foo', 'this2'), - ('/foo/', 'bar', 'that'), - ('/foo/', 'bax', 'None'), - ('/foo/bar', 'baz', "'that2'"), - ('/foo/nex', 'baz', 'that2'), + ("/", "nex", "None"), + ("/", "foo", "this"), + ("/", "bar", "that"), + ("/xyz", "foo", "this"), + ("/foo/", "foo", "this2"), + ("/foo/", "bar", "that"), + ("/foo/", "bax", "None"), + ("/foo/bar", "baz", "'that2'"), + ("/foo/nex", "baz", "that2"), # If 'foo' == 'this', then the mount point '/another' leaks into # '/'. - ('/another/', 'foo', 'None'), + ("/another/", "foo", "None"), ] for path, key, expected in tests: - self.getPage(path + '?key=' + key) + self.getPage(path + "?key=" + key) self.assertBody(expected) expectedconf = { # From CP defaults - 'tools.log_headers.on': False, - 'tools.log_tracebacks.on': True, - 'request.show_tracebacks': True, - 'log.screen': False, - 'environment': 'test_suite', - 'engine.autoreload.on': False, + "tools.log_headers.on": False, + "tools.log_tracebacks.on": True, + "request.show_tracebacks": True, + "log.screen": False, + "environment": "test_suite", + "engine.autoreload.on": False, # From global config - 'luxuryyacht': 'throatwobblermangrove', + "luxuryyacht": "throatwobblermangrove", # From Root._cp_config - 'bar': 'that', + "bar": "that", # From Foo._cp_config - 'baz': 'that2', + "baz": "that2", # From Foo.bar._cp_config - 'foo': 'this3', - 'bax': 'this4', + "foo": "this3", + "bax": "this4", } for key, expected in expectedconf.items(): - self.getPage('/foo/bar?key=' + key) + self.getPage("/foo/bar?key=" + key) self.assertBody(repr(expected)) def testUnrepr(self): - self.getPage('/repr?key=neg') - self.assertBody('-1234') + self.getPage("/repr?key=neg") + self.assertBody("-1234") - self.getPage('/repr?key=filename') - self.assertBody(repr(os.path.join(sys.prefix, 'hello.py'))) + self.getPage("/repr?key=filename") + self.assertBody(repr(os.path.join(sys.prefix, "hello.py"))) - self.getPage('/repr?key=thing1') + self.getPage("/repr?key=thing1") self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) - if not getattr(cherrypy.server, 'using_apache', False): + if not getattr(cherrypy.server, "using_apache", False): # The object ID's won't match up when using Apache, since the # server and client are running in different processes. - self.getPage('/repr?key=thing2') + self.getPage("/repr?key=thing2") from cherrypy.tutorial import thing2 + self.assertBody(repr(thing2)) - self.getPage('/repr?key=complex') - self.assertBody('(3+2j)') + self.getPage("/repr?key=complex") + self.assertBody("(3+2j)") - self.getPage('/repr?key=mul') - self.assertBody('18') + self.getPage("/repr?key=mul") + self.assertBody("18") - self.getPage('/repr?key=stradd') - self.assertBody(repr('112233')) + self.getPage("/repr?key=stradd") + self.assertBody(repr("112233")) def testRespNamespaces(self): - self.getPage('/foo/silly') - self.assertHeader('X-silly', 'sillyval') - self.assertBody('Hello world') + self.getPage("/foo/silly") + self.assertHeader("X-silly", "sillyval") + self.assertBody("Hello world") def testCustomNamespaces(self): - self.getPage('/raw/incr?num=12') - self.assertBody('13') + self.getPage("/raw/incr?num=12") + self.assertBody("13") - self.getPage('/dbscheme') - self.assertBody(r'sqlite///memory') + self.getPage("/dbscheme") + self.assertBody(r"sqlite///memory") def testHandlerToolConfigOverride(self): # Assert that config overrides tool constructor args. Above, we set # the favicon in the page handler to be '../favicon.ico', # but then overrode it in config to be './static/dirback.jpg'. - self.getPage('/favicon.ico') - with open(os.path.join(localDir, 'static/dirback.jpg'), 'rb') as tf: + self.getPage("/favicon.ico") + with open(os.path.join(localDir, "static/dirback.jpg"), "rb") as tf: self.assertBody(tf.read()) def test_request_body_namespace(self): - self.getPage('/plain', method='POST', headers=[ - ('Content-Type', 'application/x-www-form-urlencoded'), - ('Content-Length', '13')], - body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00') - self.assertBody('abc') + self.getPage( + "/plain", + method="POST", + headers=[ + ("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", "13"), + ], + body=b"\xff\xfex\x00=\xff\xfea\x00b\x00c\x00", + ) + self.assertBody("abc") class VariableSubstitutionTests(unittest.TestCase): @@ -253,9 +264,8 @@ def test_config(self): fp = StringIOFromNative(conf) cherrypy.config.update(fp) - self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir') - self.assertEqual(cherrypy.config['my'] - ['my.dir2'], '/some/dir/my/dir/dir2') + self.assertEqual(cherrypy.config["my"]["my.dir"], "/some/dir/my/dir") + self.assertEqual(cherrypy.config["my"]["my.dir2"], "/some/dir/my/dir/dir2") class CallablesInConfigTest(unittest.TestCase): @@ -263,29 +273,27 @@ class CallablesInConfigTest(unittest.TestCase): def test_call_with_literal_dict(self): from textwrap import dedent + conf = dedent(""" [my] value = dict(**{'foo': 'bar'}) """) fp = StringIOFromNative(conf) cherrypy.config.update(fp) - self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'}) + self.assertEqual(cherrypy.config["my"]["value"], {"foo": "bar"}) def test_call_with_kwargs(self): from textwrap import dedent + conf = dedent(""" [my] value = dict(foo="buzz", **cherrypy._test_dict) """) - test_dict = { - 'foo': 'bar', - 'bar': 'foo', - 'fizz': 'buzz' - } + test_dict = {"foo": "bar", "bar": "foo", "fizz": "buzz"} cherrypy._test_dict = test_dict fp = StringIOFromNative(conf) cherrypy.config.update(fp) - test_dict['foo'] = 'buzz' - self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz') - self.assertEqual(cherrypy.config['my']['value'], test_dict) + test_dict["foo"] = "buzz" + self.assertEqual(cherrypy.config["my"]["value"]["foo"], "buzz") + self.assertEqual(cherrypy.config["my"]["value"], test_dict) del cherrypy._test_dict diff --git a/cherrypy/test/test_config_server.py b/cherrypy/test/test_config_server.py index 7b1835304..d00c2f66f 100644 --- a/cherrypy/test/test_config_server.py +++ b/cherrypy/test/test_config_server.py @@ -13,114 +13,110 @@ class ServerConfigTests(helper.CPWebCase): - @staticmethod def setup_server(): - class Root: - @cherrypy.expose def index(self): - return cherrypy.request.wsgi_environ['SERVER_PORT'] + return cherrypy.request.wsgi_environ["SERVER_PORT"] @cherrypy.expose def upload(self, file): - return 'Size: %s' % len(file.file.read()) + return "Size: %s" % len(file.file.read()) @cherrypy.expose - @cherrypy.config(**{'request.body.maxbytes': 100}) + @cherrypy.config(**{"request.body.maxbytes": 100}) def tinyupload(self): return cherrypy.request.body.read() cherrypy.tree.mount(Root()) - cherrypy.config.update({ - 'server.socket_host': '0.0.0.0', - 'server.socket_port': 9876, - 'server.max_request_body_size': 200, - 'server.max_request_header_size': 500, - 'server.socket_timeout': 0.5, - - # Test explicit server.instance - 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer', - 'server.2.socket_port': 9877, - - # Test non-numeric - # Also test default server.instance = builtin server - 'server.yetanother.socket_port': 9878, - }) + cherrypy.config.update( + { + "server.socket_host": "0.0.0.0", + "server.socket_port": 9876, + "server.max_request_body_size": 200, + "server.max_request_header_size": 500, + "server.socket_timeout": 0.5, + # Test explicit server.instance + "server.2.instance": "cherrypy._cpwsgi_server.CPWSGIServer", + "server.2.socket_port": 9877, + # Test non-numeric + # Also test default server.instance = builtin server + "server.yetanother.socket_port": 9878, + } + ) PORT = 9876 def testBasicConfig(self): - self.getPage('/') + self.getPage("/") self.assertBody(str(self.PORT)) def testAdditionalServers(self): - if self.scheme == 'https': - return self.skip('not available under ssl') + if self.scheme == "https": + return self.skip("not available under ssl") self.PORT = 9877 - self.getPage('/') + self.getPage("/") self.assertBody(str(self.PORT)) self.PORT = 9878 - self.getPage('/') + self.getPage("/") self.assertBody(str(self.PORT)) def testMaxRequestSizePerHandler(self): - if getattr(cherrypy.server, 'using_apache', False): - return self.skip('skipped due to known Apache differences... ') - - self.getPage('/tinyupload', method='POST', - headers=[('Content-Type', 'text/plain'), - ('Content-Length', '100')], - body='x' * 100) + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences... ") + + self.getPage( + "/tinyupload", + method="POST", + headers=[("Content-Type", "text/plain"), ("Content-Length", "100")], + body="x" * 100, + ) self.assertStatus(200) - self.assertBody('x' * 100) + self.assertBody("x" * 100) - self.getPage('/tinyupload', method='POST', - headers=[('Content-Type', 'text/plain'), - ('Content-Length', '101')], - body='x' * 101) + self.getPage( + "/tinyupload", + method="POST", + headers=[("Content-Type", "text/plain"), ("Content-Length", "101")], + body="x" * 101, + ) self.assertStatus(413) def testMaxRequestSize(self): - if getattr(cherrypy.server, 'using_apache', False): - return self.skip('skipped due to known Apache differences... ') + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences... ") for size in (500, 5000, 50000): - self.getPage('/', headers=[('From', 'x' * 500)]) + self.getPage("/", headers=[("From", "x" * 500)]) self.assertStatus(413) # Test for https://github.com/cherrypy/cherrypy/issues/421 # (Incorrect border condition in readline of SizeCheckWrapper). # This hangs in rev 891 and earlier. - lines256 = 'x' * 248 - self.getPage('/', - headers=[('Host', '%s:%s' % (self.HOST, self.PORT)), - ('From', lines256)]) + lines256 = "x" * 248 + self.getPage( + "/", + headers=[("Host", "%s:%s" % (self.HOST, self.PORT)), ("From", lines256)], + ) # Test upload - cd = ( - 'Content-Disposition: form-data; ' - 'name="file"; ' - 'filename="hello.txt"' - ) - body = '\r\n'.join([ - '--x', - cd, - 'Content-Type: text/plain', - '', - '%s', - '--x--']) + cd = "Content-Disposition: form-data; " 'name="file"; ' 'filename="hello.txt"' + body = "\r\n".join(["--x", cd, "Content-Type: text/plain", "", "%s", "--x--"]) partlen = 200 - len(body) - b = body % ('x' * partlen) - h = [('Content-type', 'multipart/form-data; boundary=x'), - ('Content-Length', '%s' % len(b))] - self.getPage('/upload', h, 'POST', b) - self.assertBody('Size: %d' % partlen) - - b = body % ('x' * 200) - h = [('Content-type', 'multipart/form-data; boundary=x'), - ('Content-Length', '%s' % len(b))] - self.getPage('/upload', h, 'POST', b) + b = body % ("x" * partlen) + h = [ + ("Content-type", "multipart/form-data; boundary=x"), + ("Content-Length", "%s" % len(b)), + ] + self.getPage("/upload", h, "POST", b) + self.assertBody("Size: %d" % partlen) + + b = body % ("x" * 200) + h = [ + ("Content-type", "multipart/form-data; boundary=x"), + ("Content-Length", "%s" % len(b)), + ] + self.getPage("/upload", h, "POST", b) self.assertStatus(413) diff --git a/cherrypy/test/test_conn.py b/cherrypy/test/test_conn.py index e4426c422..0dc4bae1d 100644 --- a/cherrypy/test/test_conn.py +++ b/cherrypy/test/test_conn.py @@ -15,36 +15,35 @@ timeout = 1 -pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' +pov = "pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN" def setup_server(): - def raise500(): raise cherrypy.HTTPError(500) class Root: - @cherrypy.expose def index(self): return pov + page1 = index page2 = index page3 = index @cherrypy.expose def hello(self): - return 'Hello, world!' + return "Hello, world!" @cherrypy.expose def timeout(self, t): return str(cherrypy.server.httpserver.timeout) @cherrypy.expose - @cherrypy.config(**{'response.stream': True}) + @cherrypy.config(**{"response.stream": True}) def stream(self, set_cl=False): if set_cl: - cherrypy.response.headers['Content-Length'] = 10 + cherrypy.response.headers["Content-Length"] = 10 def content(): for x in range(10): @@ -58,78 +57,81 @@ def error(self, code=500): @cherrypy.expose def upload(self): - if not cherrypy.request.method == 'POST': - raise AssertionError("'POST' != request.method %r" % - cherrypy.request.method) + if not cherrypy.request.method == "POST": + raise AssertionError( + "'POST' != request.method %r" % cherrypy.request.method + ) return "thanks for '%s'" % cherrypy.request.body.read() @cherrypy.expose def custom(self, response_code): cherrypy.response.status = response_code - return 'Code = %s' % response_code + return "Code = %s" % response_code @cherrypy.expose - @cherrypy.config(**{'hooks.on_start_resource': raise500}) + @cherrypy.config(**{"hooks.on_start_resource": raise500}) def err_before_read(self): - return 'ok' + return "ok" @cherrypy.expose def one_megabyte_of_a(self): - return ['a' * 1024] * 1024 + return ["a" * 1024] * 1024 @cherrypy.expose # Turn off the encoding tool so it doens't collapse # our response body and reclaculate the Content-Length. - @cherrypy.config(**{'tools.encode.on': False}) + @cherrypy.config(**{"tools.encode.on": False}) def custom_cl(self, body, cl): - cherrypy.response.headers['Content-Length'] = cl + cherrypy.response.headers["Content-Length"] = cl if not isinstance(body, list): body = [body] newbody = [] for chunk in body: if isinstance(chunk, str): - chunk = chunk.encode('ISO-8859-1') + chunk = chunk.encode("ISO-8859-1") newbody.append(chunk) return newbody cherrypy.tree.mount(Root()) - cherrypy.config.update({ - 'server.max_request_body_size': 1001, - 'server.socket_timeout': timeout, - }) + cherrypy.config.update( + { + "server.max_request_body_size": 1001, + "server.socket_timeout": timeout, + } + ) class ConnectionCloseTests(helper.CPWebCase): setup_server = staticmethod(setup_server) def test_HTTP11(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': + if cherrypy.server.protocol_version != "HTTP/1.1": return self.skip() - self.PROTOCOL = 'HTTP/1.1' + self.PROTOCOL = "HTTP/1.1" self.persistent = True # Make the first request and assert there's no "Connection: close". - self.getPage('/') - self.assertStatus('200 OK') + self.getPage("/") + self.assertStatus("200 OK") self.assertBody(pov) - self.assertNoHeader('Connection') + self.assertNoHeader("Connection") # Make another request on the same connection. - self.getPage('/page1') - self.assertStatus('200 OK') + self.getPage("/page1") + self.assertStatus("200 OK") self.assertBody(pov) - self.assertNoHeader('Connection') + self.assertNoHeader("Connection") # Test client-side close. - self.getPage('/page2', headers=[('Connection', 'close')]) - self.assertStatus('200 OK') + self.getPage("/page2", headers=[("Connection", "close")]) + self.assertStatus("200 OK") self.assertBody(pov) - self.assertHeader('Connection', 'close') + self.assertHeader("Connection", "close") # Make another request on the same connection, which should error. - self.assertRaises(NotConnected, self.getPage, '/') + self.assertRaises(NotConnected, self.getPage, "/") def test_Streaming_no_len(self): try: @@ -150,105 +152,106 @@ def test_Streaming_with_len(self): pass def _streaming(self, set_cl): - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.PROTOCOL = 'HTTP/1.1' + if cherrypy.server.protocol_version == "HTTP/1.1": + self.PROTOCOL = "HTTP/1.1" self.persistent = True # Make the first request and assert there's no "Connection: close". - self.getPage('/') - self.assertStatus('200 OK') + self.getPage("/") + self.assertStatus("200 OK") self.assertBody(pov) - self.assertNoHeader('Connection') + self.assertNoHeader("Connection") # Make another, streamed request on the same connection. if set_cl: # When a Content-Length is provided, the content should stream # without closing the connection. - self.getPage('/stream?set_cl=Yes') - self.assertHeader('Content-Length') - self.assertNoHeader('Connection', 'close') - self.assertNoHeader('Transfer-Encoding') + self.getPage("/stream?set_cl=Yes") + self.assertHeader("Content-Length") + self.assertNoHeader("Connection", "close") + self.assertNoHeader("Transfer-Encoding") - self.assertStatus('200 OK') - self.assertBody('0123456789') + self.assertStatus("200 OK") + self.assertBody("0123456789") else: # When no Content-Length response header is provided, # streamed output will either close the connection, or use # chunked encoding, to determine transfer-length. - self.getPage('/stream') - self.assertNoHeader('Content-Length') - self.assertStatus('200 OK') - self.assertBody('0123456789') + self.getPage("/stream") + self.assertNoHeader("Content-Length") + self.assertStatus("200 OK") + self.assertBody("0123456789") chunked_response = False for k, v in self.headers: - if k.lower() == 'transfer-encoding': - if str(v) == 'chunked': + if k.lower() == "transfer-encoding": + if str(v) == "chunked": chunked_response = True if chunked_response: - self.assertNoHeader('Connection', 'close') + self.assertNoHeader("Connection", "close") else: - self.assertHeader('Connection', 'close') + self.assertHeader("Connection", "close") # Make another request on the same connection, which should # error. - self.assertRaises(NotConnected, self.getPage, '/') + self.assertRaises(NotConnected, self.getPage, "/") # Try HEAD. See # https://github.com/cherrypy/cherrypy/issues/864. - self.getPage('/stream', method='HEAD') - self.assertStatus('200 OK') - self.assertBody('') - self.assertNoHeader('Transfer-Encoding') + self.getPage("/stream", method="HEAD") + self.assertStatus("200 OK") + self.assertBody("") + self.assertNoHeader("Transfer-Encoding") else: - self.PROTOCOL = 'HTTP/1.0' + self.PROTOCOL = "HTTP/1.0" self.persistent = True # Make the first request and assert Keep-Alive. - self.getPage('/', headers=[('Connection', 'Keep-Alive')]) - self.assertStatus('200 OK') + self.getPage("/", headers=[("Connection", "Keep-Alive")]) + self.assertStatus("200 OK") self.assertBody(pov) - self.assertHeader('Connection', 'Keep-Alive') + self.assertHeader("Connection", "Keep-Alive") # Make another, streamed request on the same connection. if set_cl: # When a Content-Length is provided, the content should # stream without closing the connection. - self.getPage('/stream?set_cl=Yes', - headers=[('Connection', 'Keep-Alive')]) - self.assertHeader('Content-Length') - self.assertHeader('Connection', 'Keep-Alive') - self.assertNoHeader('Transfer-Encoding') - self.assertStatus('200 OK') - self.assertBody('0123456789') + self.getPage( + "/stream?set_cl=Yes", headers=[("Connection", "Keep-Alive")] + ) + self.assertHeader("Content-Length") + self.assertHeader("Connection", "Keep-Alive") + self.assertNoHeader("Transfer-Encoding") + self.assertStatus("200 OK") + self.assertBody("0123456789") else: # When a Content-Length is not provided, # the server should close the connection. - self.getPage('/stream', headers=[('Connection', 'Keep-Alive')]) - self.assertStatus('200 OK') - self.assertBody('0123456789') + self.getPage("/stream", headers=[("Connection", "Keep-Alive")]) + self.assertStatus("200 OK") + self.assertBody("0123456789") - self.assertNoHeader('Content-Length') - self.assertNoHeader('Connection', 'Keep-Alive') - self.assertNoHeader('Transfer-Encoding') + self.assertNoHeader("Content-Length") + self.assertNoHeader("Connection", "Keep-Alive") + self.assertNoHeader("Transfer-Encoding") # Make another request on the same connection, which should # error. - self.assertRaises(NotConnected, self.getPage, '/') + self.assertRaises(NotConnected, self.getPage, "/") def test_HTTP10_KeepAlive(self): - self.PROTOCOL = 'HTTP/1.0' - if self.scheme == 'https': + self.PROTOCOL = "HTTP/1.0" + if self.scheme == "https": self.HTTP_CONN = HTTPSConnection else: self.HTTP_CONN = HTTPConnection # Test a normal HTTP/1.0 request. - self.getPage('/page2') - self.assertStatus('200 OK') + self.getPage("/page2") + self.assertStatus("200 OK") self.assertBody(pov) # Apache, for example, may emit a Connection header even for HTTP/1.0 # self.assertNoHeader("Connection") @@ -256,14 +259,14 @@ def test_HTTP10_KeepAlive(self): # Test a keep-alive HTTP/1.0 request. self.persistent = True - self.getPage('/page3', headers=[('Connection', 'Keep-Alive')]) - self.assertStatus('200 OK') + self.getPage("/page3", headers=[("Connection", "Keep-Alive")]) + self.assertStatus("200 OK") self.assertBody(pov) - self.assertHeader('Connection', 'Keep-Alive') + self.assertHeader("Connection", "Keep-Alive") # Remove the keep-alive header again. - self.getPage('/page3') - self.assertStatus('200 OK') + self.getPage("/page3") + self.assertStatus("200 OK") self.assertBody(pov) # Apache, for example, may emit a Connection header even for HTTP/1.0 # self.assertNoHeader("Connection") @@ -275,10 +278,10 @@ class PipelineTests(helper.CPWebCase): def test_HTTP11_Timeout(self): # If we timeout without sending any data, # the server will close the conn with a 408. - if cherrypy.server.protocol_version != 'HTTP/1.1': + if cherrypy.server.protocol_version != "HTTP/1.1": return self.skip() - self.PROTOCOL = 'HTTP/1.1' + self.PROTOCOL = "HTTP/1.1" # Connect but send nothing. self.persistent = True @@ -290,7 +293,7 @@ def test_HTTP11_Timeout(self): time.sleep(timeout * 2) # The request should have returned 408 already. - response = conn.response_class(conn.sock, method='GET') + response = conn.response_class(conn.sock, method="GET") response.begin() self.assertEqual(response.status, 408) conn.close() @@ -300,14 +303,14 @@ def test_HTTP11_Timeout(self): conn = self.HTTP_CONN conn.auto_open = False conn.connect() - conn.send(b'GET /hello HTTP/1.1') - conn.send(('Host: %s' % self.HOST).encode('ascii')) + conn.send(b"GET /hello HTTP/1.1") + conn.send(("Host: %s" % self.HOST).encode("ascii")) # Wait for our socket timeout time.sleep(timeout * 2) # The conn should have already sent 408. - response = conn.response_class(conn.sock, method='GET') + response = conn.response_class(conn.sock, method="GET") response.begin() self.assertEqual(response.status, 408) conn.close() @@ -315,48 +318,46 @@ def test_HTTP11_Timeout(self): def test_HTTP11_Timeout_after_request(self): # If we timeout after at least one request has succeeded, # the server will close the conn without 408. - if cherrypy.server.protocol_version != 'HTTP/1.1': + if cherrypy.server.protocol_version != "HTTP/1.1": return self.skip() - self.PROTOCOL = 'HTTP/1.1' + self.PROTOCOL = "HTTP/1.1" # Make an initial request self.persistent = True conn = self.HTTP_CONN - conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True) - conn.putheader('Host', self.HOST) + conn.putrequest("GET", "/timeout?t=%s" % timeout, skip_host=True) + conn.putheader("Host", self.HOST) conn.endheaders() - response = conn.response_class(conn.sock, method='GET') + response = conn.response_class(conn.sock, method="GET") response.begin() self.assertEqual(response.status, 200) self.body = response.read() self.assertBody(str(timeout)) # Make a second request on the same socket - conn._output(b'GET /hello HTTP/1.1') - conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._output(b"GET /hello HTTP/1.1") + conn._output(ntob("Host: %s" % self.HOST, "ascii")) conn._send_output() - response = conn.response_class(conn.sock, method='GET') + response = conn.response_class(conn.sock, method="GET") response.begin() self.assertEqual(response.status, 200) self.body = response.read() - self.assertBody('Hello, world!') + self.assertBody("Hello, world!") # Wait for our socket timeout time.sleep(timeout * 2) # Make another request on the same socket, which should error - conn._output(b'GET /hello HTTP/1.1') - conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._output(b"GET /hello HTTP/1.1") + conn._output(ntob("Host: %s" % self.HOST, "ascii")) conn._send_output() - response = conn.response_class(conn.sock, method='GET') - msg = ( - "Writing to timed out socket didn't fail as it should have: %s") + response = conn.response_class(conn.sock, method="GET") + msg = "Writing to timed out socket didn't fail as it should have: %s" try: response.begin() except Exception: - if not isinstance(sys.exc_info()[1], - (socket.error, BadStatusLine)): + if not isinstance(sys.exc_info()[1], (socket.error, BadStatusLine)): self.fail(msg % sys.exc_info()[1]) else: if response.status != 408: @@ -367,10 +368,10 @@ def test_HTTP11_Timeout_after_request(self): # Make another request on a new socket, which should work self.persistent = True conn = self.HTTP_CONN - conn.putrequest('GET', '/', skip_host=True) - conn.putheader('Host', self.HOST) + conn.putrequest("GET", "/", skip_host=True) + conn.putheader("Host", self.HOST) conn.endheaders() - response = conn.response_class(conn.sock, method='GET') + response = conn.response_class(conn.sock, method="GET") response.begin() self.assertEqual(response.status, 200) self.body = response.read() @@ -378,15 +379,14 @@ def test_HTTP11_Timeout_after_request(self): # Make another request on the same socket, # but timeout on the headers - conn.send(b'GET /hello HTTP/1.1') + conn.send(b"GET /hello HTTP/1.1") # Wait for our socket timeout time.sleep(timeout * 2) - response = conn.response_class(conn.sock, method='GET') + response = conn.response_class(conn.sock, method="GET") try: response.begin() except Exception: - if not isinstance(sys.exc_info()[1], - (socket.error, BadStatusLine)): + if not isinstance(sys.exc_info()[1], (socket.error, BadStatusLine)): self.fail(msg % sys.exc_info()[1]) else: if response.status != 408: @@ -397,10 +397,10 @@ def test_HTTP11_Timeout_after_request(self): # Retry the request on a new connection, which should work self.persistent = True conn = self.HTTP_CONN - conn.putrequest('GET', '/', skip_host=True) - conn.putheader('Host', self.HOST) + conn.putrequest("GET", "/", skip_host=True) + conn.putheader("Host", self.HOST) conn.endheaders() - response = conn.response_class(conn.sock, method='GET') + response = conn.response_class(conn.sock, method="GET") response.begin() self.assertEqual(response.status, 200) self.body = response.read() @@ -408,52 +408,52 @@ def test_HTTP11_Timeout_after_request(self): conn.close() def test_HTTP11_pipelining(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': + if cherrypy.server.protocol_version != "HTTP/1.1": return self.skip() - self.PROTOCOL = 'HTTP/1.1' + self.PROTOCOL = "HTTP/1.1" # Test pipelining. httplib doesn't support this directly. self.persistent = True conn = self.HTTP_CONN # Put request 1 - conn.putrequest('GET', '/hello', skip_host=True) - conn.putheader('Host', self.HOST) + conn.putrequest("GET", "/hello", skip_host=True) + conn.putheader("Host", self.HOST) conn.endheaders() for trial in range(5): # Put next request - conn._output(b'GET /hello HTTP/1.1') - conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._output(b"GET /hello HTTP/1.1") + conn._output(ntob("Host: %s" % self.HOST, "ascii")) conn._send_output() # Retrieve previous response - response = conn.response_class(conn.sock, method='GET') + response = conn.response_class(conn.sock, method="GET") # there is a bug in python3 regarding the buffering of # ``conn.sock``. Until that bug get's fixed we will # monkey patch the ``response`` instance. # https://bugs.python.org/issue23377 - 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) - self.assertEqual(body, b'Hello, world!') + self.assertEqual(body, b"Hello, world!") # Retrieve final response - response = conn.response_class(conn.sock, method='GET') + response = conn.response_class(conn.sock, method="GET") response.begin() body = response.read() self.assertEqual(response.status, 200) - self.assertEqual(body, b'Hello, world!') + self.assertEqual(body, b"Hello, world!") conn.close() def test_100_Continue(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': + if cherrypy.server.protocol_version != "HTTP/1.1": return self.skip() - self.PROTOCOL = 'HTTP/1.1' + self.PROTOCOL = "HTTP/1.1" self.persistent = True conn = self.HTTP_CONN @@ -462,13 +462,13 @@ def test_100_Continue(self): # Note that httplib's response.begin automatically ignores # 100 Continue responses, so we must manually check for it. try: - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '4') + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "4") conn.endheaders() conn.send(ntob("d'oh")) - response = conn.response_class(conn.sock, method='POST') + response = conn.response_class(conn.sock, method="POST") version, status, reason = response._read_status() self.assertNotEqual(status, 100) finally: @@ -477,13 +477,13 @@ def test_100_Continue(self): # Now try a page with an Expect header... try: conn.connect() - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '17') - conn.putheader('Expect', '100-continue') + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "17") + conn.putheader("Expect", "100-continue") conn.endheaders() - response = conn.response_class(conn.sock, method='POST') + response = conn.response_class(conn.sock, method="POST") # ...assert and then skip the 100 response version, status, reason = response._read_status() @@ -492,13 +492,13 @@ def test_100_Continue(self): line = response.fp.readline().strip() if line: self.fail( - '100 Continue should not output any headers. Got %r' % - line) + "100 Continue should not output any headers. Got %r" % line + ) else: break # ...send the body - body = b'I am a small file' + body = b"I am a small file" conn.send(body) # ...get the final response @@ -514,12 +514,12 @@ class ConnectionTests(helper.CPWebCase): setup_server = staticmethod(setup_server) def test_readall_or_close(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': + if cherrypy.server.protocol_version != "HTTP/1.1": return self.skip() - self.PROTOCOL = 'HTTP/1.1' + self.PROTOCOL = "HTTP/1.1" - if self.scheme == 'https': + if self.scheme == "https": self.HTTP_CONN = HTTPSConnection else: self.HTTP_CONN = HTTPConnection @@ -533,13 +533,13 @@ def test_readall_or_close(self): conn = self.HTTP_CONN # Get a POST page with an error - conn.putrequest('POST', '/err_before_read', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '1000') - conn.putheader('Expect', '100-continue') + conn.putrequest("POST", "/err_before_read", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "1000") + conn.putheader("Expect", "100-continue") conn.endheaders() - response = conn.response_class(conn.sock, method='POST') + response = conn.response_class(conn.sock, method="POST") # ...assert and then skip the 100 response version, status, reason = response._read_status() @@ -550,7 +550,7 @@ def test_readall_or_close(self): break # ...send the body - conn.send(ntob('x' * 1000)) + conn.send(ntob("x" * 1000)) # ...get the final response response.begin() @@ -558,13 +558,13 @@ def test_readall_or_close(self): self.assertStatus(500) # Now try a working page with an Expect header... - conn._output(b'POST /upload HTTP/1.1') - conn._output(ntob('Host: %s' % self.HOST, 'ascii')) - conn._output(b'Content-Type: text/plain') - conn._output(b'Content-Length: 17') - conn._output(b'Expect: 100-continue') + conn._output(b"POST /upload HTTP/1.1") + conn._output(ntob("Host: %s" % self.HOST, "ascii")) + conn._output(b"Content-Type: text/plain") + conn._output(b"Content-Length: 17") + conn._output(b"Expect: 100-continue") conn._send_output() - response = conn.response_class(conn.sock, method='POST') + response = conn.response_class(conn.sock, method="POST") # ...assert and then skip the 100 response version, status, reason = response._read_status() @@ -575,7 +575,7 @@ def test_readall_or_close(self): break # ...send the body - body = b'I am a small file' + body = b"I am a small file" conn.send(body) # ...get the final response @@ -586,75 +586,79 @@ def test_readall_or_close(self): conn.close() def test_No_Message_Body(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': + if cherrypy.server.protocol_version != "HTTP/1.1": return self.skip() - self.PROTOCOL = 'HTTP/1.1' + self.PROTOCOL = "HTTP/1.1" # Set our HTTP_CONN to an instance so it persists between requests. self.persistent = True # Make the first request and assert there's no "Connection: close". - self.getPage('/') - self.assertStatus('200 OK') + self.getPage("/") + self.assertStatus("200 OK") self.assertBody(pov) - self.assertNoHeader('Connection') + self.assertNoHeader("Connection") # Make a 204 request on the same connection. - self.getPage('/custom/204') + self.getPage("/custom/204") self.assertStatus(204) - self.assertNoHeader('Content-Length') - self.assertBody('') - self.assertNoHeader('Connection') + self.assertNoHeader("Content-Length") + self.assertBody("") + self.assertNoHeader("Connection") # Make a 304 request on the same connection. - self.getPage('/custom/304') + self.getPage("/custom/304") self.assertStatus(304) - self.assertNoHeader('Content-Length') - self.assertBody('') - self.assertNoHeader('Connection') + self.assertNoHeader("Content-Length") + self.assertBody("") + self.assertNoHeader("Connection") def test_Chunked_Encoding(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': + if cherrypy.server.protocol_version != "HTTP/1.1": return self.skip() - if (hasattr(self, 'harness') and - 'modpython' in self.harness.__class__.__name__.lower()): + if ( + hasattr(self, "harness") + and "modpython" in self.harness.__class__.__name__.lower() + ): # mod_python forbids chunked encoding return self.skip() - self.PROTOCOL = 'HTTP/1.1' + self.PROTOCOL = "HTTP/1.1" # Set our HTTP_CONN to an instance so it persists between requests. self.persistent = True conn = self.HTTP_CONN # Try a normal chunked request (with extensions) - body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n' - 'Content-Type: application/json\r\n' - '\r\n') - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Transfer-Encoding', 'chunked') - conn.putheader('Trailer', 'Content-Type') + body = ntob( + "8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n" + "Content-Type: application/json\r\n" + "\r\n" + ) + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Transfer-Encoding", "chunked") + conn.putheader("Trailer", "Content-Type") # Note that this is somewhat malformed: # we shouldn't be sending Content-Length. # RFC 2616 says the server should ignore it. - conn.putheader('Content-Length', '3') + conn.putheader("Content-Length", "3") conn.endheaders() conn.send(body) response = conn.getresponse() self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus('200 OK') - self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy') + self.assertStatus("200 OK") + self.assertBody("thanks for '%s'" % b"xx\r\nxxxxyyyyy") # Try a chunked request that exceeds server.max_request_body_size. # Note that the delimiters and trailer are included. - body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n') - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Transfer-Encoding', 'chunked') - conn.putheader('Content-Type', 'text/plain') + body = ntob("3e3\r\n" + ("x" * 995) + "\r\n0\r\n\r\n") + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Transfer-Encoding", "chunked") + conn.putheader("Content-Type", "text/plain") # Chunked requests don't need a content-length # # conn.putheader("Content-Length", len(body)) conn.endheaders() @@ -669,16 +673,17 @@ def test_Content_Length_in(self): # server.max_request_body_size. Assert error before body send. self.persistent = True conn = self.HTTP_CONN - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '9999') + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "9999") conn.endheaders() response = conn.getresponse() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(413) - self.assertBody('The entity sent with the request exceeds ' - 'the maximum allowed bytes.') + self.assertBody( + "The entity sent with the request exceeds " "the maximum allowed bytes." + ) conn.close() def test_Content_Length_out_preheaders(self): @@ -686,16 +691,18 @@ def test_Content_Length_out_preheaders(self): # the actual bytes in the response body. self.persistent = True conn = self.HTTP_CONN - conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5', - skip_host=True) - conn.putheader('Host', self.HOST) + conn.putrequest( + "GET", "/custom_cl?body=I+have+too+many+bytes&cl=5", skip_host=True + ) + conn.putheader("Host", self.HOST) conn.endheaders() response = conn.getresponse() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(500) self.assertBody( - 'The requested resource returned more bytes than the ' - 'declared Content-Length.') + "The requested resource returned more bytes than the " + "declared Content-Length." + ) conn.close() def test_Content_Length_out_postheaders(self): @@ -704,18 +711,18 @@ def test_Content_Length_out_postheaders(self): self.persistent = True conn = self.HTTP_CONN conn.putrequest( - 'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5', - skip_host=True) - conn.putheader('Host', self.HOST) + "GET", "/custom_cl?body=I+too&body=+have+too+many&cl=5", skip_host=True + ) + conn.putheader("Host", self.HOST) conn.endheaders() response = conn.getresponse() self.status, self.headers, self.body = webtest.shb(response) self.assertStatus(200) - self.assertBody('I too') + self.assertBody("I too") conn.close() def test_598(self): - tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/' + tmpl = "{scheme}://{host}:{port}/one_megabyte_of_a/" url = tmpl.format( scheme=self.scheme, host=self.HOST, @@ -734,42 +741,42 @@ def test_598(self): remaining -= len(data) self.assertEqual(len(buf), 1024 * 1024) - self.assertEqual(buf, ntob('a' * 1024 * 1024)) + self.assertEqual(buf, ntob("a" * 1024 * 1024)) self.assertEqual(remaining, 0) remote_data_conn.close() def setup_upload_server(): - class Root: @cherrypy.expose def upload(self): - if not cherrypy.request.method == 'POST': - raise AssertionError("'POST' != request.method %r" % - cherrypy.request.method) + if not cherrypy.request.method == "POST": + raise AssertionError( + "'POST' != request.method %r" % cherrypy.request.method + ) return "thanks for '%s'" % tonative(cherrypy.request.body.read()) cherrypy.tree.mount(Root()) - cherrypy.config.update({ - 'server.max_request_body_size': 1001, - 'server.socket_timeout': 10, - 'server.accepted_queue_size': 5, - 'server.accepted_queue_timeout': 0.1, - }) + cherrypy.config.update( + { + "server.max_request_body_size": 1001, + "server.socket_timeout": 10, + "server.accepted_queue_size": 5, + "server.accepted_queue_timeout": 0.1, + } + ) -reset_names = 'ECONNRESET', 'WSAECONNRESET' +reset_names = "ECONNRESET", "WSAECONNRESET" socket_reset_errors = [ - getattr(errno, name) - for name in reset_names - if hasattr(errno, name) + getattr(errno, name) for name in reset_names if hasattr(errno, name) ] -'reset error numbers available on this platform' +"reset error numbers available on this platform" socket_reset_errors += [ # Python 3.5 raises an http.client.RemoteDisconnected # with this message - 'Remote end closed connection without response', + "Remote end closed connection without response", ] @@ -785,10 +792,10 @@ def test_queue_full(self): # all of wsgiserver's WorkerThreads and fill its Queue. for i in range(15): conn = self.HTTP_CONN(self.HOST, self.PORT) - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '4') + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "4") conn.endheaders() conns.append(conn) @@ -796,20 +803,19 @@ def test_queue_full(self): # server immediately. overflow_conn = self.HTTP_CONN(self.HOST, self.PORT) # Manually connect since httplib won't let us set a timeout - for res in socket.getaddrinfo(self.HOST, self.PORT, 0, - socket.SOCK_STREAM): + for res in socket.getaddrinfo(self.HOST, self.PORT, 0, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res overflow_conn.sock = socket.socket(af, socktype, proto) overflow_conn.sock.settimeout(5) overflow_conn.sock.connect(sa) break - overflow_conn.putrequest('GET', '/', skip_host=True) - overflow_conn.putheader('Host', self.HOST) + overflow_conn.putrequest("GET", "/", skip_host=True) + overflow_conn.putheader("Host", self.HOST) overflow_conn.endheaders() response = overflow_conn.response_class( overflow_conn.sock, - method='GET', + method="GET", ) try: response.begin() @@ -817,21 +823,18 @@ def test_queue_full(self): if exc.args[0] in socket_reset_errors: pass # Expected. else: - tmpl = ( - 'Overflow conn did not get RST. ' - 'Got {exc.args!r} instead' - ) + tmpl = "Overflow conn did not get RST. " "Got {exc.args!r} instead" raise AssertionError(tmpl.format(**locals())) except BadStatusLine: # This is a special case in OS X. Linux and Windows will # RST correctly. - assert sys.platform == 'darwin' + assert sys.platform == "darwin" else: - raise AssertionError('Overflow conn did not get RST ') + raise AssertionError("Overflow conn did not get RST ") finally: for conn in conns: - conn.send(b'done') - response = conn.response_class(conn.sock, method='POST') + conn.send(b"done") + response = conn.response_class(conn.sock, method="POST") response.begin() self.body = response.read() self.assertBody("thanks for 'done'") @@ -848,17 +851,17 @@ def test_No_CRLF(self): self.persistent = True conn = self.HTTP_CONN - conn.send(b'GET /hello HTTP/1.1\n\n') - response = conn.response_class(conn.sock, method='GET') + conn.send(b"GET /hello HTTP/1.1\n\n") + response = conn.response_class(conn.sock, method="GET") response.begin() self.body = response.read() - self.assertBody('HTTP requires CRLF terminators') + self.assertBody("HTTP requires CRLF terminators") conn.close() conn.connect() - conn.send(b'GET /hello HTTP/1.1\r\n\n') - response = conn.response_class(conn.sock, method='GET') + conn.send(b"GET /hello HTTP/1.1\r\n\n") + response = conn.response_class(conn.sock, method="GET") response.begin() self.body = response.read() - self.assertBody('HTTP requires CRLF terminators') + self.assertBody("HTTP requires CRLF terminators") conn.close() diff --git a/cherrypy/test/test_core.py b/cherrypy/test/test_core.py index 1753957aa..68c50fe05 100644 --- a/cherrypy/test/test_core.py +++ b/cherrypy/test/test_core.py @@ -15,29 +15,30 @@ localDir = os.path.dirname(__file__) -favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico') +favicon_path = os.path.join(os.getcwd(), localDir, "../favicon.ico") # Client-side code # class CoreRequestHandlingTest(helper.CPWebCase): - @staticmethod def setup_server(): class Root: - @cherrypy.expose def index(self): - return 'hello' + return "hello" favicon_ico = tools.staticfile.handler(filename=favicon_path) @cherrypy.expose def defct(self, newct): - newct = 'text/%s' % newct - cherrypy.config.update({'tools.response_headers.on': True, - 'tools.response_headers.headers': - [('Content-Type', newct)]}) + newct = "text/%s" % newct + cherrypy.config.update( + { + "tools.response_headers.on": True, + "tools.response_headers.headers": [("Content-Type", newct)], + } + ) @cherrypy.expose def baseurl(self, path_info, relative=None): @@ -51,24 +52,25 @@ class TestType(type): subclass, and adds an instance of the subclass as an attribute of root. """ + def __init__(cls, name, bases, dct): type.__init__(cls, name, bases, dct) for value in dct.values(): if isinstance(value, types.FunctionType): value.exposed = True setattr(root, name.lower(), cls()) - Test = TestType('Test', (object, ), {}) - @cherrypy.config(**{'tools.trailing_slash.on': False}) - class URL(Test): + Test = TestType("Test", (object,), {}) + @cherrypy.config(**{"tools.trailing_slash.on": False}) + class URL(Test): def index(self, path_info, relative=None): - if relative != 'server': + if relative != "server": relative = bool(relative) return cherrypy.url(path_info, relative=relative) def leaf(self, path_info, relative=None): - if relative != 'server': + if relative != "server": relative = bool(relative) return cherrypy.url(path_info, relative=relative) @@ -77,16 +79,15 @@ def qs(self, qs): def log_status(): Status.statuses.append(cherrypy.response.status) - cherrypy.tools.log_status = cherrypy.Tool( - 'on_end_resource', log_status) - class Status(Test): + cherrypy.tools.log_status = cherrypy.Tool("on_end_resource", log_status) + class Status(Test): def index(self): - return 'normal' + return "normal" def blank(self): - cherrypy.response.status = '' + cherrypy.response.status = "" # According to RFC 2616, new status codes are OK as long as they # are between 100 and 599. @@ -94,146 +95,150 @@ def blank(self): # Here is an illegal code... def illegal(self): cherrypy.response.status = 781 - return 'oops' + return "oops" # ...and here is an unknown but legal code. def unknown(self): - cherrypy.response.status = '431 My custom error' - return 'funky' + cherrypy.response.status = "431 My custom error" + return "funky" # Non-numeric code def bad(self): - cherrypy.response.status = 'error' - return 'bad news' + cherrypy.response.status = "error" + return "bad news" statuses = [] - @cherrypy.config(**{'tools.log_status.on': True}) + @cherrypy.config(**{"tools.log_status.on": True}) def on_end_resource_stage(self): return repr(self.statuses) class Redirect(Test): - - @cherrypy.config(**{ - 'tools.err_redirect.on': True, - 'tools.err_redirect.url': '/errpage', - 'tools.err_redirect.internal': False, - }) + @cherrypy.config( + **{ + "tools.err_redirect.on": True, + "tools.err_redirect.url": "/errpage", + "tools.err_redirect.internal": False, + } + ) class Error: @cherrypy.expose def index(self): - raise NameError('redirect_test') + raise NameError("redirect_test") error = Error() def index(self): - return 'child' + return "child" def custom(self, url, code): raise cherrypy.HTTPRedirect(url, code) - @cherrypy.config(**{'tools.trailing_slash.extra': True}) + @cherrypy.config(**{"tools.trailing_slash.extra": True}) def by_code(self, code): - raise cherrypy.HTTPRedirect('somewhere%20else', code) + raise cherrypy.HTTPRedirect("somewhere%20else", code) def nomodify(self): - raise cherrypy.HTTPRedirect('', 304) + raise cherrypy.HTTPRedirect("", 304) def proxy(self): - raise cherrypy.HTTPRedirect('proxy', 305) + raise cherrypy.HTTPRedirect("proxy", 305) def stringify(self): - return str(cherrypy.HTTPRedirect('/')) + return str(cherrypy.HTTPRedirect("/")) def fragment(self, frag): - raise cherrypy.HTTPRedirect('/some/url#%s' % frag) + raise cherrypy.HTTPRedirect("/some/url#%s" % frag) def url_with_quote(self): raise cherrypy.HTTPRedirect("/some\"url/that'we/want") def url_with_xss(self): raise cherrypy.HTTPRedirect( - "/someurl/that'we/want") + "/someurl/that'we/want" + ) def url_with_unicode(self): - raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8')) + raise cherrypy.HTTPRedirect(ntou("тест", "utf-8")) def login_redir(): - if not getattr(cherrypy.request, 'login', None): - raise cherrypy.InternalRedirect('/internalredirect/login') - tools.login_redir = _cptools.Tool('before_handler', login_redir) + if not getattr(cherrypy.request, "login", None): + raise cherrypy.InternalRedirect("/internalredirect/login") + + tools.login_redir = _cptools.Tool("before_handler", login_redir) def redir_custom(): - raise cherrypy.InternalRedirect('/internalredirect/custom_err') + raise cherrypy.InternalRedirect("/internalredirect/custom_err") class InternalRedirect(Test): - def index(self): - raise cherrypy.InternalRedirect('/') + raise cherrypy.InternalRedirect("/") @cherrypy.expose - @cherrypy.config(**{'hooks.before_error_response': redir_custom}) + @cherrypy.config(**{"hooks.before_error_response": redir_custom}) def choke(self): return 3 / 0 def relative(self, a, b): - raise cherrypy.InternalRedirect('cousin?t=6') + raise cherrypy.InternalRedirect("cousin?t=6") def cousin(self, t): assert cherrypy.request.prev.closed return cherrypy.request.prev.query_string def petshop(self, user_id): - if user_id == 'parrot': + if user_id == "parrot": # Trade it for a slug when redirecting raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=slug') - elif user_id == 'terrier': + "/image/getImagesByUser?user_id=slug" + ) + elif user_id == "terrier": # Trade it for a fish when redirecting raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=fish') + "/image/getImagesByUser?user_id=fish" + ) else: # This should pass the user_id through to getImagesByUser raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=%s' % str(user_id)) + "/image/getImagesByUser?user_id=%s" % str(user_id) + ) # We support Python 2.3, but the @-deco syntax would look like # this: # @tools.login_redir() def secure(self): - return 'Welcome!' + return "Welcome!" + secure = tools.login_redir()(secure) # Since calling the tool returns the same function you pass in, # you could skip binding the return value, and just write: # tools.login_redir()(secure) def login(self): - return 'Please log in' + return "Please log in" def custom_err(self): - return 'Something went horribly wrong.' + return "Something went horribly wrong." - @cherrypy.config(**{'hooks.before_request_body': redir_custom}) + @cherrypy.config(**{"hooks.before_request_body": redir_custom}) def early_ir(self, arg): - return 'whatever' + return "whatever" class Image(Test): - def getImagesByUser(self, user_id): - return '0 images for %s' % user_id + return "0 images for %s" % user_id class Flatten(Test): - def as_string(self): - return 'content' + return "content" def as_list(self): - return ['con', 'tent'] + return ["con", "tent"] def as_yield(self): - yield b'content' + yield b"content" - @cherrypy.config(**{'tools.flatten.on': True}) + @cherrypy.config(**{"tools.flatten.on": True}) def as_dblyield(self): yield self.as_yield() @@ -242,17 +247,14 @@ def as_refyield(self): yield chunk class Ranges(Test): - def get_ranges(self, bytes): - return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) + return repr(httputil.get_ranges("bytes=%s" % bytes, 8)) def slice_file(self): path = os.path.join(os.getcwd(), os.path.dirname(__file__)) - return static.serve_file( - os.path.join(path, 'static/index.html')) + return static.serve_file(os.path.join(path, "static/index.html")) class Cookies(Test): - def single(self, name): cookie = cherrypy.request.cookie[name] # Python2's SimpleCookie.__setitem__ won't take unicode keys. @@ -264,203 +266,215 @@ def multiple(self, names): def append_headers(header_list, debug=False): if debug: cherrypy.log( - 'Extending response headers with %s' % repr(header_list), - 'TOOLS.APPEND_HEADERS') + "Extending response headers with %s" % repr(header_list), + "TOOLS.APPEND_HEADERS", + ) cherrypy.serving.response.header_list.extend(header_list) - cherrypy.tools.append_headers = cherrypy.Tool( - 'on_end_resource', append_headers) - class MultiHeader(Test): + cherrypy.tools.append_headers = cherrypy.Tool("on_end_resource", append_headers) + class MultiHeader(Test): def header_list(self): pass - header_list = cherrypy.tools.append_headers(header_list=[ - (b'WWW-Authenticate', b'Negotiate'), - (b'WWW-Authenticate', b'Basic realm="foo"'), - ])(header_list) + + header_list = cherrypy.tools.append_headers( + header_list=[ + (b"WWW-Authenticate", b"Negotiate"), + (b"WWW-Authenticate", b'Basic realm="foo"'), + ] + )(header_list) def commas(self): - cherrypy.response.headers[ - 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' + cherrypy.response.headers["WWW-Authenticate"] = ( + 'Negotiate,Basic realm="foo"' + ) cherrypy.tree.mount(root) def testStatus(self): - self.getPage('/status/') - self.assertBody('normal') + self.getPage("/status/") + self.assertBody("normal") self.assertStatus(200) - self.getPage('/status/blank') - self.assertBody('') + self.getPage("/status/blank") + self.assertBody("") self.assertStatus(200) - self.getPage('/status/illegal') + self.getPage("/status/illegal") self.assertStatus(500) - msg = 'Illegal response status from server (781 is out of range).' + msg = "Illegal response status from server (781 is out of range)." self.assertErrorPage(500, msg) - if not getattr(cherrypy.server, 'using_apache', False): - self.getPage('/status/unknown') - self.assertBody('funky') + if not getattr(cherrypy.server, "using_apache", False): + self.getPage("/status/unknown") + self.assertBody("funky") self.assertStatus(431) - self.getPage('/status/bad') + self.getPage("/status/bad") self.assertStatus(500) msg = "Illegal response status from server ('error' is non-numeric)." self.assertErrorPage(500, msg) def test_on_end_resource_status(self): - self.getPage('/status/on_end_resource_stage') - self.assertBody('[]') - self.getPage('/status/on_end_resource_stage') - self.assertBody(repr(['200 OK'])) + self.getPage("/status/on_end_resource_stage") + self.assertBody("[]") + self.getPage("/status/on_end_resource_stage") + self.assertBody(repr(["200 OK"])) def testSlashes(self): # Test that requests for index methods without a trailing slash # get redirected to the same URI path with a trailing slash. # Make sure GET params are preserved. - self.getPage('/redirect?id=3') + self.getPage("/redirect?id=3") self.assertStatus(301) self.assertMatchesBody( - '' - '%s/redirect/[?]id=3' % (self.base(), self.base()) + "" + "%s/redirect/[?]id=3" % (self.base(), self.base()) ) if self.prefix(): # Corner case: the "trailing slash" redirect could be tricky if # we're using a virtual root and the URI is "/vroot" (no slash). - self.getPage('') + self.getPage("") self.assertStatus(301) - self.assertMatchesBody("%s/" % - (self.base(), self.base())) + self.assertMatchesBody( + "%s/" % (self.base(), self.base()) + ) # Test that requests for NON-index methods WITH a trailing slash # get redirected to the same URI path WITHOUT a trailing slash. # Make sure GET params are preserved. - self.getPage('/redirect/by_code/?code=307') + self.getPage("/redirect/by_code/?code=307") self.assertStatus(301) self.assertMatchesBody( "" - '%s/redirect/by_code[?]code=307' - % (self.base(), self.base()) + "%s/redirect/by_code[?]code=307" % (self.base(), self.base()) ) # If the trailing_slash tool is off, CP should just continue # as if the slashes were correct. But it needs some help # inside cherrypy.url to form correct output. - self.getPage('/url?path_info=page1') - self.assertBody('%s/url/page1' % self.base()) - self.getPage('/url/leaf/?path_info=page1') - self.assertBody('%s/url/page1' % self.base()) + self.getPage("/url?path_info=page1") + self.assertBody("%s/url/page1" % self.base()) + self.getPage("/url/leaf/?path_info=page1") + self.assertBody("%s/url/page1" % self.base()) def testRedirect(self): - self.getPage('/redirect/') - self.assertBody('child') + self.getPage("/redirect/") + self.assertBody("child") self.assertStatus(200) - self.getPage('/redirect/by_code?code=300') + self.getPage("/redirect/by_code?code=300") self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else" + ) self.assertStatus(300) - self.getPage('/redirect/by_code?code=301') + self.getPage("/redirect/by_code?code=301") self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else" + ) self.assertStatus(301) - self.getPage('/redirect/by_code?code=302') + self.getPage("/redirect/by_code?code=302") self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else" + ) self.assertStatus(302) - self.getPage('/redirect/by_code?code=303') + self.getPage("/redirect/by_code?code=303") self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else" + ) self.assertStatus(303) - self.getPage('/redirect/by_code?code=307') + self.getPage("/redirect/by_code?code=307") self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else" + ) self.assertStatus(307) - self.getPage('/redirect/by_code?code=308') + self.getPage("/redirect/by_code?code=308") self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else" + ) self.assertStatus(308) - self.getPage('/redirect/nomodify') - self.assertBody('') + self.getPage("/redirect/nomodify") + self.assertBody("") self.assertStatus(304) - self.getPage('/redirect/proxy') - self.assertBody('') + self.getPage("/redirect/proxy") + self.assertBody("") self.assertStatus(305) # HTTPRedirect on error - self.getPage('/redirect/error/') - self.assertStatus(('302 Found', '303 See Other')) - self.assertInBody('/errpage') + self.getPage("/redirect/error/") + self.assertStatus(("302 Found", "303 See Other")) + self.assertInBody("/errpage") # Make sure str(HTTPRedirect()) works. - self.getPage('/redirect/stringify', protocol='HTTP/1.0') + self.getPage("/redirect/stringify", protocol="HTTP/1.0") self.assertStatus(200) self.assertBody("(['%s/'], 302)" % self.base()) - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.getPage('/redirect/stringify', protocol='HTTP/1.1') + if cherrypy.server.protocol_version == "HTTP/1.1": + self.getPage("/redirect/stringify", protocol="HTTP/1.1") self.assertStatus(200) self.assertBody("(['%s/'], 303)" % self.base()) # check that #fragments are handled properly # http://skrb.org/ietf/http_errata.html#location-fragments - frag = 'foo' - self.getPage('/redirect/fragment/%s' % frag) + frag = "foo" + self.getPage("/redirect/fragment/%s" % frag) self.assertMatchesBody( - r"\2\/some\/url\#%s" % ( - frag, frag)) - loc = self.assertHeader('Location') - assert loc.endswith('#%s' % frag) - self.assertStatus(('302 Found', '303 See Other')) + r"\2\/some\/url\#%s" % (frag, frag) + ) + loc = self.assertHeader("Location") + assert loc.endswith("#%s" % frag) + self.assertStatus(("302 Found", "303 See Other")) # check injection protection # See https://github.com/cherrypy/cherrypy/issues/1003 self.getPage( - '/redirect/custom?' - 'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval') + "/redirect/custom?" + "code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval" + ) self.assertStatus(303) - loc = self.assertHeader('Location') - assert 'Set-Cookie' in loc - self.assertNoHeader('Set-Cookie') + loc = self.assertHeader("Location") + assert "Set-Cookie" in loc + self.assertNoHeader("Set-Cookie") def assertValidXHTML(): from xml.etree import ElementTree + try: ElementTree.fromstring( - '%s' % self.body, + "%s" % self.body, ) except ElementTree.ParseError: self._handlewebError( - 'automatically generated redirect did not ' - 'generate well-formed html', + "automatically generated redirect did not " + "generate well-formed html", ) # check redirects to URLs generated valid HTML - we check this # by seeing if it appears as valid XHTML. - self.getPage('/redirect/by_code?code=303') + self.getPage("/redirect/by_code?code=303") self.assertStatus(303) assertValidXHTML() # do the same with a url containing quote characters. - self.getPage('/redirect/url_with_quote') + self.getPage("/redirect/url_with_quote") self.assertStatus(303) assertValidXHTML() def test_redirect_with_xss(self): """A redirect to a URL with HTML injected should result in page contents escaped.""" - self.getPage('/redirect/url_with_xss') + self.getPage("/redirect/url_with_xss") self.assertStatus(303) - assert b'url/that'we/want" - ) + "/someurl/that'we/want") def url_with_unicode(self): - raise cherrypy.HTTPRedirect(ntou("тест", "utf-8")) + raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8')) def login_redir(): - if not getattr(cherrypy.request, "login", None): - raise cherrypy.InternalRedirect("/internalredirect/login") - - tools.login_redir = _cptools.Tool("before_handler", login_redir) + if not getattr(cherrypy.request, 'login', None): + raise cherrypy.InternalRedirect('/internalredirect/login') + tools.login_redir = _cptools.Tool('before_handler', login_redir) def redir_custom(): - raise cherrypy.InternalRedirect("/internalredirect/custom_err") + raise cherrypy.InternalRedirect('/internalredirect/custom_err') class InternalRedirect(Test): + def index(self): - raise cherrypy.InternalRedirect("/") + raise cherrypy.InternalRedirect('/') @cherrypy.expose - @cherrypy.config(**{"hooks.before_error_response": redir_custom}) + @cherrypy.config(**{'hooks.before_error_response': redir_custom}) def choke(self): return 3 / 0 def relative(self, a, b): - raise cherrypy.InternalRedirect("cousin?t=6") + raise cherrypy.InternalRedirect('cousin?t=6') def cousin(self, t): assert cherrypy.request.prev.closed return cherrypy.request.prev.query_string def petshop(self, user_id): - if user_id == "parrot": + if user_id == 'parrot': # Trade it for a slug when redirecting raise cherrypy.InternalRedirect( - "/image/getImagesByUser?user_id=slug" - ) - elif user_id == "terrier": + '/image/getImagesByUser?user_id=slug') + elif user_id == 'terrier': # Trade it for a fish when redirecting raise cherrypy.InternalRedirect( - "/image/getImagesByUser?user_id=fish" - ) + '/image/getImagesByUser?user_id=fish') else: # This should pass the user_id through to getImagesByUser raise cherrypy.InternalRedirect( - "/image/getImagesByUser?user_id=%s" % str(user_id) - ) + '/image/getImagesByUser?user_id=%s' % str(user_id)) # We support Python 2.3, but the @-deco syntax would look like # this: # @tools.login_redir() def secure(self): - return "Welcome!" - + return 'Welcome!' secure = tools.login_redir()(secure) # Since calling the tool returns the same function you pass in, # you could skip binding the return value, and just write: # tools.login_redir()(secure) def login(self): - return "Please log in" + return 'Please log in' def custom_err(self): - return "Something went horribly wrong." + return 'Something went horribly wrong.' - @cherrypy.config(**{"hooks.before_request_body": redir_custom}) + @cherrypy.config(**{'hooks.before_request_body': redir_custom}) def early_ir(self, arg): - return "whatever" + return 'whatever' class Image(Test): + def getImagesByUser(self, user_id): - return "0 images for %s" % user_id + return '0 images for %s' % user_id class Flatten(Test): + def as_string(self): - return "content" + return 'content' def as_list(self): - return ["con", "tent"] + return ['con', 'tent'] def as_yield(self): - yield b"content" + yield b'content' - @cherrypy.config(**{"tools.flatten.on": True}) + @cherrypy.config(**{'tools.flatten.on': True}) def as_dblyield(self): yield self.as_yield() @@ -247,14 +242,17 @@ def as_refyield(self): yield chunk class Ranges(Test): + def get_ranges(self, bytes): - return repr(httputil.get_ranges("bytes=%s" % bytes, 8)) + return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) def slice_file(self): path = os.path.join(os.getcwd(), os.path.dirname(__file__)) - return static.serve_file(os.path.join(path, "static/index.html")) + return static.serve_file( + os.path.join(path, 'static/index.html')) class Cookies(Test): + def single(self, name): cookie = cherrypy.request.cookie[name] # Python2's SimpleCookie.__setitem__ won't take unicode keys. @@ -266,215 +264,203 @@ def multiple(self, names): def append_headers(header_list, debug=False): if debug: cherrypy.log( - "Extending response headers with %s" % repr(header_list), - "TOOLS.APPEND_HEADERS", - ) + 'Extending response headers with %s' % repr(header_list), + 'TOOLS.APPEND_HEADERS') cherrypy.serving.response.header_list.extend(header_list) - - cherrypy.tools.append_headers = cherrypy.Tool("on_end_resource", append_headers) + cherrypy.tools.append_headers = cherrypy.Tool( + 'on_end_resource', append_headers) class MultiHeader(Test): + def header_list(self): pass - - header_list = cherrypy.tools.append_headers( - header_list=[ - (b"WWW-Authenticate", b"Negotiate"), - (b"WWW-Authenticate", b'Basic realm="foo"'), - ] - )(header_list) + header_list = cherrypy.tools.append_headers(header_list=[ + (b'WWW-Authenticate', b'Negotiate'), + (b'WWW-Authenticate', b'Basic realm="foo"'), + ])(header_list) def commas(self): - cherrypy.response.headers["WWW-Authenticate"] = ( - 'Negotiate,Basic realm="foo"' - ) + cherrypy.response.headers[ + 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' cherrypy.tree.mount(root) def testStatus(self): - self.getPage("/status/") - self.assertBody("normal") + self.getPage('/status/') + self.assertBody('normal') self.assertStatus(200) - self.getPage("/status/blank") - self.assertBody("") + self.getPage('/status/blank') + self.assertBody('') self.assertStatus(200) - self.getPage("/status/illegal") + self.getPage('/status/illegal') self.assertStatus(500) - msg = "Illegal response status from server (781 is out of range)." + msg = 'Illegal response status from server (781 is out of range).' self.assertErrorPage(500, msg) - if not getattr(cherrypy.server, "using_apache", False): - self.getPage("/status/unknown") - self.assertBody("funky") + if not getattr(cherrypy.server, 'using_apache', False): + self.getPage('/status/unknown') + self.assertBody('funky') self.assertStatus(431) - self.getPage("/status/bad") + self.getPage('/status/bad') self.assertStatus(500) msg = "Illegal response status from server ('error' is non-numeric)." self.assertErrorPage(500, msg) def test_on_end_resource_status(self): - self.getPage("/status/on_end_resource_stage") - self.assertBody("[]") - self.getPage("/status/on_end_resource_stage") - self.assertBody(repr(["200 OK"])) + self.getPage('/status/on_end_resource_stage') + self.assertBody('[]') + self.getPage('/status/on_end_resource_stage') + self.assertBody(repr(['200 OK'])) def testSlashes(self): # Test that requests for index methods without a trailing slash # get redirected to the same URI path with a trailing slash. # Make sure GET params are preserved. - self.getPage("/redirect?id=3") + self.getPage('/redirect?id=3') self.assertStatus(301) self.assertMatchesBody( - "" - "%s/redirect/[?]id=3" % (self.base(), self.base()) + '' + '%s/redirect/[?]id=3' % (self.base(), self.base()) ) if self.prefix(): # Corner case: the "trailing slash" redirect could be tricky if # we're using a virtual root and the URI is "/vroot" (no slash). - self.getPage("") + self.getPage('') self.assertStatus(301) - self.assertMatchesBody( - "%s/" % (self.base(), self.base()) - ) + self.assertMatchesBody("%s/" % + (self.base(), self.base())) # Test that requests for NON-index methods WITH a trailing slash # get redirected to the same URI path WITHOUT a trailing slash. # Make sure GET params are preserved. - self.getPage("/redirect/by_code/?code=307") + self.getPage('/redirect/by_code/?code=307') self.assertStatus(301) self.assertMatchesBody( "" - "%s/redirect/by_code[?]code=307" % (self.base(), self.base()) + '%s/redirect/by_code[?]code=307' + % (self.base(), self.base()) ) # If the trailing_slash tool is off, CP should just continue # as if the slashes were correct. But it needs some help # inside cherrypy.url to form correct output. - self.getPage("/url?path_info=page1") - self.assertBody("%s/url/page1" % self.base()) - self.getPage("/url/leaf/?path_info=page1") - self.assertBody("%s/url/page1" % self.base()) + self.getPage('/url?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf/?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) def testRedirect(self): - self.getPage("/redirect/") - self.assertBody("child") + self.getPage('/redirect/') + self.assertBody('child') self.assertStatus(200) - self.getPage("/redirect/by_code?code=300") + self.getPage('/redirect/by_code?code=300') self.assertMatchesBody( - r"\2somewhere%20else" - ) + r"\2somewhere%20else") self.assertStatus(300) - self.getPage("/redirect/by_code?code=301") + self.getPage('/redirect/by_code?code=301') self.assertMatchesBody( - r"\2somewhere%20else" - ) + r"\2somewhere%20else") self.assertStatus(301) - self.getPage("/redirect/by_code?code=302") + self.getPage('/redirect/by_code?code=302') self.assertMatchesBody( - r"\2somewhere%20else" - ) + r"\2somewhere%20else") self.assertStatus(302) - self.getPage("/redirect/by_code?code=303") + self.getPage('/redirect/by_code?code=303') self.assertMatchesBody( - r"\2somewhere%20else" - ) + r"\2somewhere%20else") self.assertStatus(303) - self.getPage("/redirect/by_code?code=307") + self.getPage('/redirect/by_code?code=307') self.assertMatchesBody( - r"\2somewhere%20else" - ) + r"\2somewhere%20else") self.assertStatus(307) - self.getPage("/redirect/by_code?code=308") + self.getPage('/redirect/by_code?code=308') self.assertMatchesBody( - r"\2somewhere%20else" - ) + r"\2somewhere%20else") self.assertStatus(308) - self.getPage("/redirect/nomodify") - self.assertBody("") + self.getPage('/redirect/nomodify') + self.assertBody('') self.assertStatus(304) - self.getPage("/redirect/proxy") - self.assertBody("") + self.getPage('/redirect/proxy') + self.assertBody('') self.assertStatus(305) # HTTPRedirect on error - self.getPage("/redirect/error/") - self.assertStatus(("302 Found", "303 See Other")) - self.assertInBody("/errpage") + self.getPage('/redirect/error/') + self.assertStatus(('302 Found', '303 See Other')) + self.assertInBody('/errpage') # Make sure str(HTTPRedirect()) works. - self.getPage("/redirect/stringify", protocol="HTTP/1.0") + self.getPage('/redirect/stringify', protocol='HTTP/1.0') self.assertStatus(200) self.assertBody("(['%s/'], 302)" % self.base()) - if cherrypy.server.protocol_version == "HTTP/1.1": - self.getPage("/redirect/stringify", protocol="HTTP/1.1") + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.getPage('/redirect/stringify', protocol='HTTP/1.1') self.assertStatus(200) self.assertBody("(['%s/'], 303)" % self.base()) # check that #fragments are handled properly # http://skrb.org/ietf/http_errata.html#location-fragments - frag = "foo" - self.getPage("/redirect/fragment/%s" % frag) + frag = 'foo' + self.getPage('/redirect/fragment/%s' % frag) self.assertMatchesBody( - r"\2\/some\/url\#%s" % (frag, frag) - ) - loc = self.assertHeader("Location") - assert loc.endswith("#%s" % frag) - self.assertStatus(("302 Found", "303 See Other")) + r"\2\/some\/url\#%s" % ( + frag, frag)) + loc = self.assertHeader('Location') + assert loc.endswith('#%s' % frag) + self.assertStatus(('302 Found', '303 See Other')) # check injection protection # See https://github.com/cherrypy/cherrypy/issues/1003 self.getPage( - "/redirect/custom?" - "code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval" - ) + '/redirect/custom?' + 'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval') self.assertStatus(303) - loc = self.assertHeader("Location") - assert "Set-Cookie" in loc - self.assertNoHeader("Set-Cookie") + loc = self.assertHeader('Location') + assert 'Set-Cookie' in loc + self.assertNoHeader('Set-Cookie') def assertValidXHTML(): from xml.etree import ElementTree - try: ElementTree.fromstring( - "%s" % self.body, + '%s' % self.body, ) except ElementTree.ParseError: self._handlewebError( - "automatically generated redirect did not " - "generate well-formed html", + 'automatically generated redirect did not ' + 'generate well-formed html', ) # check redirects to URLs generated valid HTML - we check this # by seeing if it appears as valid XHTML. - self.getPage("/redirect/by_code?code=303") + self.getPage('/redirect/by_code?code=303') self.assertStatus(303) assertValidXHTML() # do the same with a url containing quote characters. - self.getPage("/redirect/url_with_quote") + self.getPage('/redirect/url_with_quote') self.assertStatus(303) assertValidXHTML() def test_redirect_with_xss(self): """A redirect to a URL with HTML injected should result in page contents escaped.""" - self.getPage("/redirect/url_with_xss") + self.getPage('/redirect/url_with_xss') self.assertStatus(303) - assert b"url/that'we/want") + "/someurl/that'we/want", + ) def url_with_unicode(self): raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8')) @@ -161,13 +169,13 @@ def url_with_unicode(self): def login_redir(): if not getattr(cherrypy.request, 'login', None): raise cherrypy.InternalRedirect('/internalredirect/login') + tools.login_redir = _cptools.Tool('before_handler', login_redir) def redir_custom(): raise cherrypy.InternalRedirect('/internalredirect/custom_err') class InternalRedirect(Test): - def index(self): raise cherrypy.InternalRedirect('/') @@ -187,21 +195,25 @@ def petshop(self, user_id): if user_id == 'parrot': # Trade it for a slug when redirecting raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=slug') + '/image/getImagesByUser?user_id=slug', + ) elif user_id == 'terrier': # Trade it for a fish when redirecting raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=fish') + '/image/getImagesByUser?user_id=fish', + ) else: # This should pass the user_id through to getImagesByUser raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=%s' % str(user_id)) + '/image/getImagesByUser?user_id=%s' % str(user_id), + ) # We support Python 2.3, but the @-deco syntax would look like # this: # @tools.login_redir() def secure(self): return 'Welcome!' + secure = tools.login_redir()(secure) # Since calling the tool returns the same function you pass in, # you could skip binding the return value, and just write: @@ -218,12 +230,10 @@ def early_ir(self, arg): return 'whatever' class Image(Test): - def getImagesByUser(self, user_id): return '0 images for %s' % user_id class Flatten(Test): - def as_string(self): return 'content' @@ -242,17 +252,16 @@ def as_refyield(self): yield chunk class Ranges(Test): - def get_ranges(self, bytes): return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) def slice_file(self): path = os.path.join(os.getcwd(), os.path.dirname(__file__)) return static.serve_file( - os.path.join(path, 'static/index.html')) + os.path.join(path, 'static/index.html'), + ) class Cookies(Test): - def single(self, name): cookie = cherrypy.request.cookie[name] # Python2's SimpleCookie.__setitem__ won't take unicode keys. @@ -265,23 +274,30 @@ def append_headers(header_list, debug=False): if debug: cherrypy.log( 'Extending response headers with %s' % repr(header_list), - 'TOOLS.APPEND_HEADERS') + 'TOOLS.APPEND_HEADERS', + ) cherrypy.serving.response.header_list.extend(header_list) + cherrypy.tools.append_headers = cherrypy.Tool( - 'on_end_resource', append_headers) + 'on_end_resource', + append_headers, + ) class MultiHeader(Test): - def header_list(self): pass - header_list = cherrypy.tools.append_headers(header_list=[ - (b'WWW-Authenticate', b'Negotiate'), - (b'WWW-Authenticate', b'Basic realm="foo"'), - ])(header_list) + + header_list = cherrypy.tools.append_headers( + header_list=[ + (b'WWW-Authenticate', b'Negotiate'), + (b'WWW-Authenticate', b'Basic realm="foo"'), + ], + )(header_list) def commas(self): - cherrypy.response.headers[ - 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' + cherrypy.response.headers['WWW-Authenticate'] = ( + 'Negotiate,Basic realm="foo"' + ) cherrypy.tree.mount(root) @@ -323,7 +339,7 @@ def testSlashes(self): self.assertStatus(301) self.assertMatchesBody( '' - '%s/redirect/[?]id=3' % (self.base(), self.base()) + '%s/redirect/[?]id=3' % (self.base(), self.base()), ) if self.prefix(): @@ -331,8 +347,9 @@ def testSlashes(self): # we're using a virtual root and the URI is "/vroot" (no slash). self.getPage('') self.assertStatus(301) - self.assertMatchesBody("%s/" % - (self.base(), self.base())) + self.assertMatchesBody( + '%s/' % (self.base(), self.base()), + ) # Test that requests for NON-index methods WITH a trailing slash # get redirected to the same URI path WITHOUT a trailing slash. @@ -340,9 +357,8 @@ def testSlashes(self): self.getPage('/redirect/by_code/?code=307') self.assertStatus(301) self.assertMatchesBody( - "" - '%s/redirect/by_code[?]code=307' - % (self.base(), self.base()) + '' + '%s/redirect/by_code[?]code=307' % (self.base(), self.base()), ) # If the trailing_slash tool is off, CP should just continue @@ -360,32 +376,38 @@ def testRedirect(self): self.getPage('/redirect/by_code?code=300') self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else", + ) self.assertStatus(300) self.getPage('/redirect/by_code?code=301') self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else", + ) self.assertStatus(301) self.getPage('/redirect/by_code?code=302') self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else", + ) self.assertStatus(302) self.getPage('/redirect/by_code?code=303') self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else", + ) self.assertStatus(303) self.getPage('/redirect/by_code?code=307') self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else", + ) self.assertStatus(307) self.getPage('/redirect/by_code?code=308') self.assertMatchesBody( - r"\2somewhere%20else") + r"\2somewhere%20else", + ) self.assertStatus(308) self.getPage('/redirect/nomodify') @@ -415,8 +437,9 @@ def testRedirect(self): frag = 'foo' self.getPage('/redirect/fragment/%s' % frag) self.assertMatchesBody( - r"\2\/some\/url\#%s" % ( - frag, frag)) + r"\2\/some\/url\#%s" + % (frag, frag), + ) loc = self.assertHeader('Location') assert loc.endswith('#%s' % frag) self.assertStatus(('302 Found', '303 See Other')) @@ -425,7 +448,8 @@ def testRedirect(self): # See https://github.com/cherrypy/cherrypy/issues/1003 self.getPage( '/redirect/custom?' - 'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval') + 'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval', + ) self.assertStatus(303) loc = self.assertHeader('Location') assert 'Set-Cookie' in loc @@ -433,6 +457,7 @@ def testRedirect(self): def assertValidXHTML(): from xml.etree import ElementTree + try: ElementTree.fromstring( '%s' % self.body.decode('utf-8'), @@ -482,7 +507,8 @@ def test_InternalRedirect(self): # Test passthrough self.getPage( - '/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film') + '/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film', + ) self.assertBody('0 images for Sir-not-appearing-in-this-film') self.assertStatus(200) @@ -492,14 +518,20 @@ def test_InternalRedirect(self): self.assertStatus(200) # Test POST - self.getPage('/internalredirect/petshop', method='POST', - body='user_id=terrier') + self.getPage( + '/internalredirect/petshop', + method='POST', + body='user_id=terrier', + ) self.assertBody('0 images for fish') self.assertStatus(200) # Test ir before body read - self.getPage('/internalredirect/early_ir', method='POST', - body='arg=aha!') + self.getPage( + '/internalredirect/early_ir', + method='POST', + body='arg=aha!', + ) self.assertBody('Something went horribly wrong.') self.assertStatus(200) @@ -519,9 +551,13 @@ def test_InternalRedirect(self): self.assertBody('Something went horribly wrong.') def testFlatten(self): - for url in ['/flatten/as_string', '/flatten/as_list', - '/flatten/as_yield', '/flatten/as_dblyield', - '/flatten/as_refyield']: + for url in [ + '/flatten/as_string', + '/flatten/as_list', + '/flatten/as_yield', + '/flatten/as_dblyield', + '/flatten/as_refyield', + ]: self.getPage(url) self.assertBody('content') @@ -553,18 +589,20 @@ def testRanges(self): ct = self.assertHeader('Content-Type') expected_type = 'multipart/byteranges; boundary=' assert ct.startswith(expected_type) - boundary = ct[len(expected_type):] - expected_body = ('\r\n--%s\r\n' - 'Content-type: text/html\r\n' - 'Content-range: bytes 4-6/14\r\n' - '\r\n' - 'o, \r\n' - '--%s\r\n' - 'Content-type: text/html\r\n' - 'Content-range: bytes 2-5/14\r\n' - '\r\n' - 'llo,\r\n' - '--%s--\r\n' % (boundary, boundary, boundary)) + boundary = ct[len(expected_type) :] + expected_body = ( + '\r\n--%s\r\n' + 'Content-type: text/html\r\n' + 'Content-range: bytes 4-6/14\r\n' + '\r\n' + 'o, \r\n' + '--%s\r\n' + 'Content-type: text/html\r\n' + 'Content-range: bytes 2-5/14\r\n' + '\r\n' + 'llo,\r\n' + '--%s--\r\n' % (boundary, boundary, boundary) + ) self.assertBody(expected_body) self.assertHeader('Content-Length') @@ -606,18 +644,25 @@ def skip_if_bad_cookies(self): def testCookies(self): self.skip_if_bad_cookies() - self.getPage('/cookies/single?name=First', - [('Cookie', 'First=Dinsdale;')]) + self.getPage( + '/cookies/single?name=First', + [('Cookie', 'First=Dinsdale;')], + ) self.assertHeader('Set-Cookie', 'First=Dinsdale') - self.getPage('/cookies/multiple?names=First&names=Last', - [('Cookie', 'First=Dinsdale; Last=Piranha;'), - ]) + self.getPage( + '/cookies/multiple?names=First&names=Last', + [ + ('Cookie', 'First=Dinsdale; Last=Piranha;'), + ], + ) self.assertHeader('Set-Cookie', 'First=Dinsdale') self.assertHeader('Set-Cookie', 'Last=Piranha') - self.getPage('/cookies/single?name=Something-With%2CComma', - [('Cookie', 'Something-With,Comma=some-value')]) + self.getPage( + '/cookies/single?name=Something-With%2CComma', + [('Cookie', 'Something-With,Comma=some-value')], + ) self.assertStatus(400) def testDefaultContentType(self): @@ -632,9 +677,11 @@ def test_multiple_headers(self): self.getPage('/multiheader/header_list') self.assertEqual( [(k, v) for k, v in self.headers if k == 'WWW-Authenticate'], - [('WWW-Authenticate', 'Negotiate'), - ('WWW-Authenticate', 'Basic realm="foo"'), - ]) + [ + ('WWW-Authenticate', 'Negotiate'), + ('WWW-Authenticate', 'Basic realm="foo"'), + ], + ) self.getPage('/multiheader/commas') self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"') @@ -646,8 +693,7 @@ def test_cherrypy_url(self): self.assertBody('%s/url/page1' % self.base()) # Other host header host = 'www.mydomain.example' - self.getPage('/url/leaf?path_info=page1', - headers=[('Host', host)]) + self.getPage('/url/leaf?path_info=page1', headers=[('Host', host)]) self.assertBody('%s://%s/url/page1' % (self.scheme, host)) # Input is 'absolute'; that is, relative to script_name @@ -760,17 +806,18 @@ def test_expose_decorator(self): class ErrorTests(helper.CPWebCase): - @staticmethod def setup_server(): def break_header(): # Add a header after finalize that is invalid cherrypy.serving.response.header_list.append((2, 3)) + cherrypy.tools.break_header = cherrypy.Tool( - 'on_end_resource', break_header) + 'on_end_resource', + break_header, + ) class Root: - @cherrypy.expose def index(self): return 'hello' @@ -792,15 +839,16 @@ def test_start_response_error(self): self.getPage('/start_response_error') self.assertStatus(500) self.assertInBody( - 'TypeError: response.header_list key 2 is not a byte string.') + 'TypeError: response.header_list key 2 is not a byte string.', + ) def test_contextmanager(self): self.getPage('/stat/missing') self.assertStatus(404) body_text = self.body.decode('utf-8') assert ( - 'No such file or directory' in body_text or - 'cannot find the file specified' in body_text + 'No such file or directory' in body_text + or 'cannot find the file specified' in body_text ) diff --git a/cherrypy/test/test_dynamicobjectmapping.py b/cherrypy/test/test_dynamicobjectmapping.py index 6ac339321..6364dfb69 100644 --- a/cherrypy/test/test_dynamicobjectmapping.py +++ b/cherrypy/test/test_dynamicobjectmapping.py @@ -6,7 +6,6 @@ def setup_server(): class SubSubRoot: - @cherrypy.expose def index(self): return 'SubSubRoot index' @@ -29,7 +28,6 @@ def dispatch(self): } class SubRoot: - @cherrypy.expose def index(self): return 'SubRoot index' @@ -51,7 +49,6 @@ def _cp_dispatch(self, vpath): } class Root: - @cherrypy.expose def index(self): return 'index' @@ -71,7 +68,6 @@ def _cp_dispatch(self, vpath): # DynamicNodeAndMethodDispatcher example. # This example exposes a fairly naive HTTP api class User(object): - def __init__(self, id, name): self.id = id self.name = name @@ -95,7 +91,6 @@ def make_user(name, id=None): @cherrypy.expose class UserContainerNode(object): - def POST(self, name): """Allow the creation of a new Object.""" return 'POST %d' % make_user(name) @@ -112,7 +107,6 @@ def dynamic_dispatch(self, vpath): @cherrypy.expose class UserInstanceNode(object): - def __init__(self, id): self.id = id self.user = user_lookup.get(id, None) @@ -152,9 +146,7 @@ def DELETE(self): return 'DELETE %d' % id class ABHandler: - class CustomDispatch: - @cherrypy.expose def index(self, a, b): return 'custom' @@ -177,7 +169,6 @@ def delete(self, a, b): return 'deleting ' + str(a) + ' and ' + str(b) class IndexOnly: - def _cp_dispatch(self, vpath): """Make sure that popping ALL of vpath still shows the index handler. @@ -200,8 +191,10 @@ def index(self): @cherrypy.expose def hi(self): return "hi was not interpreted as 'a' param" - DecoratedPopArgs = cherrypy.popargs( - 'a', 'b', handler=ABHandler())(DecoratedPopArgs) + + DecoratedPopArgs = cherrypy.popargs('a', 'b', handler=ABHandler())( + DecoratedPopArgs, + ) class NonDecoratedPopArgs: """Test _cp_dispatch = cherrypy.popargs()""" @@ -222,14 +215,17 @@ def __init__(self, a): def index(self): if 'a' in cherrypy.request.params: raise Exception( - 'Parameterized handler argument ended up in ' - 'request.params') + 'Parameterized handler argument ended up ' + 'in request.params', + ) return self.a class ParameterizedPopArgs: """Test cherrypy.popargs() with a function call handler.""" - ParameterizedPopArgs = cherrypy.popargs( - 'a', handler=ParameterizedHandler)(ParameterizedPopArgs) + + ParameterizedPopArgs = cherrypy.popargs('a', handler=ParameterizedHandler)( + ParameterizedPopArgs, + ) Root.decorated = DecoratedPopArgs() Root.undecorated = NonDecoratedPopArgs() @@ -244,9 +240,7 @@ class ParameterizedPopArgs: '/': { 'user': (url or '/').split('/')[-2], }, - '/users': { - 'request.dispatch': md - }, + '/users': {'request.dispatch': md}, } cherrypy.tree.mount(Root(), url, conf) @@ -360,14 +354,20 @@ def testMethodDispatch(self): self.assertHeader('Allow', headers) # Make sure POSTs update already existings resources - self.getPage('/users/%d' % - id, method='POST', body='name=%s' % updatedname) + self.getPage( + '/users/%d' % id, + method='POST', + body='name=%s' % updatedname, + ) self.assertBody('POST %d' % id) self.assertHeader('Allow', headers) # Make sure PUTs Update already existing resources. - self.getPage('/users/%d' % - id, method='PUT', body='name=%s' % updatedname) + self.getPage( + '/users/%d' % id, + method='PUT', + body='name=%s' % updatedname, + ) self.assertBody('PUT %d' % id) self.assertHeader('Allow', headers) diff --git a/cherrypy/test/test_encoding.py b/cherrypy/test/test_encoding.py index 6075103d8..c020f5357 100644 --- a/cherrypy/test/test_encoding.py +++ b/cherrypy/test/test_encoding.py @@ -20,15 +20,15 @@ class EncodingTests(helper.CPWebCase): - @staticmethod def setup_server(): class Root: - @cherrypy.expose def index(self, param): assert param == europoundUnicode, '%r != %r' % ( - param, europoundUnicode) + param, + europoundUnicode, + ) yield europoundUnicode @cherrypy.expose @@ -47,31 +47,37 @@ def cookies_and_headers(self): # should not fail. cherrypy.response.cookie['candy'] = 'bar' cherrypy.response.cookie['candy']['domain'] = 'cherrypy.dev' - cherrypy.response.headers[ - 'Some-Header'] = 'My d\xc3\xb6g has fleas' - cherrypy.response.headers[ - 'Bytes-Header'] = b'Bytes given header' + cherrypy.response.headers['Some-Header'] = ( + 'My d\xc3\xb6g has fleas' + ) + cherrypy.response.headers['Bytes-Header'] = ( + b'Bytes given header' + ) return 'Any content' @cherrypy.expose def reqparams(self, *args, **kwargs): return b', '.join( - [': '.join((k, v)).encode('utf8') - for k, v in sorted(cherrypy.request.params.items())] + [ + ': '.join((k, v)).encode('utf8') + for k, v in sorted(cherrypy.request.params.items()) + ], ) @cherrypy.expose - @cherrypy.config(**{ - 'tools.encode.text_only': False, - 'tools.encode.add_charset': True, - }) + @cherrypy.config( + **{ + 'tools.encode.text_only': False, + 'tools.encode.add_charset': True, + }, + ) def nontext(self, *args, **kwargs): - cherrypy.response.headers[ - 'Content-Type'] = 'application/binary' + cherrypy.response.headers['Content-Type'] = ( + 'application/binary' + ) return '\x00\x01\x02\x03' class GZIP: - @cherrypy.expose def index(self): yield 'Hello, world' @@ -96,24 +102,35 @@ def noshow_stream(self): yield 'Here be dragons' class Decode: - @cherrypy.expose - @cherrypy.config(**{ - 'tools.decode.on': True, - 'tools.decode.default_encoding': ['utf-16'], - }) + @cherrypy.config( + **{ + 'tools.decode.on': True, + 'tools.decode.default_encoding': ['utf-16'], + }, + ) def extra_charset(self, *args, **kwargs): - return ', '.join([': '.join((k, v)) - for k, v in cherrypy.request.params.items()]) + return ', '.join( + [ + ': '.join((k, v)) + for k, v in cherrypy.request.params.items() + ], + ) @cherrypy.expose - @cherrypy.config(**{ - 'tools.decode.on': True, - 'tools.decode.encoding': 'utf-16', - }) + @cherrypy.config( + **{ + 'tools.decode.on': True, + 'tools.decode.encoding': 'utf-16', + }, + ) def force_charset(self, *args, **kwargs): - return ', '.join([': '.join((k, v)) - for k, v in cherrypy.request.params.items()]) + return ', '.join( + [ + ': '.join((k, v)) + for k, v in cherrypy.request.params.items() + ], + ) root = Root() root.gzip = GZIP() @@ -141,122 +158,170 @@ def test_query_string_decoding(self): self.assertErrorPage( 404, 'The given query string could not be processed. Query ' - "strings for this resource must be encoded with 'utf8'.") + "strings for this resource must be encoded with 'utf8'.", + ) def test_urlencoded_decoding(self): # Test the decoding of an application/x-www-form-urlencoded entity. europoundUtf8 = europoundUnicode.encode('utf-8') body = b'param=' + europoundUtf8 - self.getPage('/', - method='POST', - headers=[ - ('Content-Type', 'application/x-www-form-urlencoded'), - ('Content-Length', str(len(body))), - ], - body=body), + ( + self.getPage( + '/', + method='POST', + headers=[ + ('Content-Type', 'application/x-www-form-urlencoded'), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertBody(europoundUtf8) # Encoded utf8 entities MUST be parsed and decoded correctly. # Here, q is the POUND SIGN U+00A3 encoded in utf8 body = b'q=\xc2\xa3' - self.getPage('/reqparams', method='POST', - headers=[( - 'Content-Type', 'application/x-www-form-urlencoded'), - ('Content-Length', str(len(body))), - ], - body=body), + ( + self.getPage( + '/reqparams', + method='POST', + headers=[ + ('Content-Type', 'application/x-www-form-urlencoded'), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertBody(b'q: \xc2\xa3') # ...and in utf16, which is not in the default attempt_charsets list: body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00' - self.getPage('/reqparams', - method='POST', - headers=[ - ('Content-Type', - 'application/x-www-form-urlencoded;charset=utf-16'), - ('Content-Length', str(len(body))), - ], - body=body), + ( + self.getPage( + '/reqparams', + method='POST', + headers=[ + ( + 'Content-Type', + 'application/x-www-form-urlencoded;charset=utf-16', + ), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertBody(b'q: \xc2\xa3') # Entities that are incorrectly encoded MUST raise 400. # Here, q is the POUND SIGN U+00A3 encoded in utf16, but # the Content-Type incorrectly labels it utf-8. body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00' - self.getPage('/reqparams', - method='POST', - headers=[ - ('Content-Type', - 'application/x-www-form-urlencoded;charset=utf-8'), - ('Content-Length', str(len(body))), - ], - body=body), + ( + self.getPage( + '/reqparams', + method='POST', + headers=[ + ( + 'Content-Type', + 'application/x-www-form-urlencoded;charset=utf-8', + ), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertStatus(400) self.assertErrorPage( 400, 'The request entity could not be decoded. The following charsets ' - "were attempted: ['utf-8']") + "were attempted: ['utf-8']", + ) def test_decode_tool(self): # An extra charset should be tried first, and succeed if it matches. # Here, we add utf-16 as a charset and pass a utf-16 body. body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00' - self.getPage('/decode/extra_charset', method='POST', - headers=[( - 'Content-Type', 'application/x-www-form-urlencoded'), - ('Content-Length', str(len(body))), - ], - body=body), + ( + self.getPage( + '/decode/extra_charset', + method='POST', + headers=[ + ('Content-Type', 'application/x-www-form-urlencoded'), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertBody(b'q: \xc2\xa3') # An extra charset should be tried first, and continue to other default # charsets if it doesn't match. # Here, we add utf-16 as a charset but still pass a utf-8 body. body = b'q=\xc2\xa3' - self.getPage('/decode/extra_charset', method='POST', - headers=[( - 'Content-Type', 'application/x-www-form-urlencoded'), - ('Content-Length', str(len(body))), - ], - body=body), + ( + self.getPage( + '/decode/extra_charset', + method='POST', + headers=[ + ('Content-Type', 'application/x-www-form-urlencoded'), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertBody(b'q: \xc2\xa3') # An extra charset should error if force is True and it doesn't match. # Here, we force utf-16 as a charset but still pass a utf-8 body. body = b'q=\xc2\xa3' - self.getPage('/decode/force_charset', method='POST', - headers=[( - 'Content-Type', 'application/x-www-form-urlencoded'), - ('Content-Length', str(len(body))), - ], - body=body), + ( + self.getPage( + '/decode/force_charset', + method='POST', + headers=[ + ('Content-Type', 'application/x-www-form-urlencoded'), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertErrorPage( 400, 'The request entity could not be decoded. The following charsets ' - "were attempted: ['utf-16']") + "were attempted: ['utf-16']", + ) def test_multipart_decoding(self): # Test the decoding of a multipart entity when the charset (utf16) is # explicitly given. - body = ntob('\r\n'.join([ - '--X', - 'Content-Type: text/plain;charset=utf-16', - 'Content-Disposition: form-data; name="text"', - '', - '\xff\xfea\x00b\x00\x1c c\x00', - '--X', - 'Content-Type: text/plain;charset=utf-16', - 'Content-Disposition: form-data; name="submit"', - '', - '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', - '--X--' - ])) - self.getPage('/reqparams', method='POST', - headers=[( - 'Content-Type', 'multipart/form-data;boundary=X'), - ('Content-Length', str(len(body))), - ], - body=body), + body = ntob( + '\r\n'.join( + [ + '--X', + 'Content-Type: text/plain;charset=utf-16', + 'Content-Disposition: form-data; name="text"', + '', + '\xff\xfea\x00b\x00\x1c c\x00', + '--X', + 'Content-Type: text/plain;charset=utf-16', + 'Content-Disposition: form-data; name="submit"', + '', + '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', + '--X--', + ], + ), + ) + ( + self.getPage( + '/reqparams', + method='POST', + headers=[ + ('Content-Type', 'multipart/form-data;boundary=X'), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertBody(b'submit: Create, text: ab\xe2\x80\x9cc') @mock.patch('cherrypy._cpreqbody.Part.maxrambytes', 1) @@ -270,50 +335,69 @@ def test_multipart_decoding_bigger_maxrambytes(self): def test_multipart_decoding_no_charset(self): # Test the decoding of a multipart entity when the charset (utf8) is # NOT explicitly given, but is in the list of charsets to attempt. - body = ntob('\r\n'.join([ - '--X', - 'Content-Disposition: form-data; name="text"', - '', - '\xe2\x80\x9c', - '--X', - 'Content-Disposition: form-data; name="submit"', - '', - 'Create', - '--X--' - ])) - self.getPage('/reqparams', method='POST', - headers=[( - 'Content-Type', 'multipart/form-data;boundary=X'), - ('Content-Length', str(len(body))), - ], - body=body), + body = ntob( + '\r\n'.join( + [ + '--X', + 'Content-Disposition: form-data; name="text"', + '', + '\xe2\x80\x9c', + '--X', + 'Content-Disposition: form-data; name="submit"', + '', + 'Create', + '--X--', + ], + ), + ) + ( + self.getPage( + '/reqparams', + method='POST', + headers=[ + ('Content-Type', 'multipart/form-data;boundary=X'), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertBody(b'submit: Create, text: \xe2\x80\x9c') def test_multipart_decoding_no_successful_charset(self): # Test the decoding of a multipart entity when the charset (utf16) is # NOT explicitly given, and is NOT in the list of charsets to attempt. - body = ntob('\r\n'.join([ - '--X', - 'Content-Disposition: form-data; name="text"', - '', - '\xff\xfea\x00b\x00\x1c c\x00', - '--X', - 'Content-Disposition: form-data; name="submit"', - '', - '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', - '--X--' - ])) - self.getPage('/reqparams', method='POST', - headers=[( - 'Content-Type', 'multipart/form-data;boundary=X'), - ('Content-Length', str(len(body))), - ], - body=body), + body = ntob( + '\r\n'.join( + [ + '--X', + 'Content-Disposition: form-data; name="text"', + '', + '\xff\xfea\x00b\x00\x1c c\x00', + '--X', + 'Content-Disposition: form-data; name="submit"', + '', + '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', + '--X--', + ], + ), + ) + ( + self.getPage( + '/reqparams', + method='POST', + headers=[ + ('Content-Type', 'multipart/form-data;boundary=X'), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertStatus(400) self.assertErrorPage( 400, 'The request entity could not be decoded. The following charsets ' - "were attempted: ['us-ascii', 'utf-8']") + "were attempted: ['us-ascii', 'utf-8']", + ) def test_nontext(self): self.getPage('/nontext') @@ -332,8 +416,10 @@ def testEncoding(self): # Ask for multiple encodings. ISO-8859-1 should fail, and utf-16 # should be produced. - self.getPage('/mao_zedong', [('Accept-Charset', - 'iso-8859-1;q=1, utf-16;q=0.5')]) + self.getPage( + '/mao_zedong', + [('Accept-Charset', 'iso-8859-1;q=1, utf-16;q=0.5')], + ) self.assertBody(sing16) # The "*" value should default to our default_encoding, utf-8 @@ -343,17 +429,23 @@ def testEncoding(self): # Only allow iso-8859-1, which should fail and raise 406. self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')]) self.assertStatus('406 Not Acceptable') - self.assertInBody('Your client sent this Accept-Charset header: ' - 'iso-8859-1, *;q=0. We tried these charsets: ' - 'iso-8859-1.') + self.assertInBody( + 'Your client sent this Accept-Charset header: ' + 'iso-8859-1, *;q=0. We tried these charsets: ' + 'iso-8859-1.', + ) # Ask for x-mac-ce, which should be unknown. See ticket #569. - self.getPage('/mao_zedong', [('Accept-Charset', - 'us-ascii, ISO-8859-1, x-mac-ce')]) + self.getPage( + '/mao_zedong', + [('Accept-Charset', 'us-ascii, ISO-8859-1, x-mac-ce')], + ) self.assertStatus('406 Not Acceptable') - self.assertInBody('Your client sent this Accept-Charset header: ' - 'us-ascii, ISO-8859-1, x-mac-ce. We tried these ' - 'charsets: ISO-8859-1, us-ascii, x-mac-ce.') + self.assertInBody( + 'Your client sent this Accept-Charset header: ' + 'us-ascii, ISO-8859-1, x-mac-ce. We tried these ' + 'charsets: ISO-8859-1, us-ascii, x-mac-ce.', + ) # Test the 'encoding' arg to encode. self.getPage('/utf8') @@ -362,8 +454,10 @@ def testEncoding(self): self.assertStatus('406 Not Acceptable') # Test malformed quality value, which should raise 400. - self.getPage('/mao_zedong', [('Accept-Charset', - 'ISO-8859-1,utf-8;q=0.7,*;q=0.7)')]) + self.getPage( + '/mao_zedong', + [('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7)')], + ) self.assertStatus('400 Bad Request') def testGzip(self): @@ -409,18 +503,26 @@ def testGzip(self): # readable page, since 1) the gzip header is already set, # and 2) we may have already written some of the body. # The fix is to never stream yields when using gzip. - if (cherrypy.server.protocol_version == 'HTTP/1.0' or - getattr(cherrypy.server, 'using_apache', False)): - self.getPage('/gzip/noshow_stream', - headers=[('Accept-Encoding', 'gzip')]) + if cherrypy.server.protocol_version == 'HTTP/1.0' or getattr( + cherrypy.server, + 'using_apache', + False, + ): + self.getPage( + '/gzip/noshow_stream', + headers=[('Accept-Encoding', 'gzip')], + ) self.assertHeader('Content-Encoding', 'gzip') self.assertInBody('\x1f\x8b\x08\x00') else: # The wsgiserver will simply stop sending data, and the HTTP client # will error due to an incomplete chunk-encoded stream. - self.assertRaises((ValueError, IncompleteRead), self.getPage, - '/gzip/noshow_stream', - headers=[('Accept-Encoding', 'gzip')]) + self.assertRaises( + (ValueError, IncompleteRead), + self.getPage, + '/gzip/noshow_stream', + headers=[('Accept-Encoding', 'gzip')], + ) def test_UnicodeHeaders(self): self.getPage('/cookies_and_headers') diff --git a/cherrypy/test/test_etags.py b/cherrypy/test/test_etags.py index 293eb8662..b8898443f 100644 --- a/cherrypy/test/test_etags.py +++ b/cherrypy/test/test_etags.py @@ -4,11 +4,9 @@ class ETagTest(helper.CPWebCase): - @staticmethod def setup_server(): class Root: - @cherrypy.expose def resource(self): return 'Oh wah ta goo Siam.' @@ -27,9 +25,12 @@ def fail(self, code): def unicoded(self): return ntou('I am a \u1ee4nicode string.', 'escape') - conf = {'/': {'tools.etags.on': True, - 'tools.etags.autotags': True, - }} + conf = { + '/': { + 'tools.etags.on': True, + 'tools.etags.autotags': True, + }, + } cherrypy.tree.mount(Root(), config=conf) def test_etags(self): @@ -52,8 +53,11 @@ def test_etags(self): # Test If-None-Match (both valid and invalid) self.getPage('/resource', headers=[('If-None-Match', etag)]) self.assertStatus(304) - self.getPage('/resource', method='POST', - headers=[('If-None-Match', etag)]) + self.getPage( + '/resource', + method='POST', + headers=[('If-None-Match', etag)], + ) self.assertStatus('412 Precondition Failed') self.getPage('/resource', headers=[('If-None-Match', '*')]) self.assertStatus(304) diff --git a/cherrypy/test/test_http.py b/cherrypy/test/test_http.py index b72254ae9..3ac4de2ba 100644 --- a/cherrypy/test/test_http.py +++ b/cherrypy/test/test_http.py @@ -33,11 +33,13 @@ def encode_filename(filename): if is_ascii(filename): return 'filename', '"{filename}"'.format(**locals()) encoded = urllib.parse.quote(filename, encoding='utf-8') - return 'filename*', "'".join(( - 'UTF-8', - '', # lang - encoded, - )) + return 'filename*', "'".join( + ( + 'UTF-8', + '', # lang + encoded, + ), + ) def encode_multipart_formdata(files): @@ -52,8 +54,9 @@ def encode_multipart_formdata(files): L.append('--' + BOUNDARY) fn_key, encoded = encode_filename(filename) - tmpl = \ + tmpl = ( 'Content-Disposition: form-data; name="{key}"; {fn_key}={encoded}' + ) L.append(tmpl.format(**locals())) ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream' L.append('Content-Type: %s' % ct) @@ -67,7 +70,6 @@ def encode_multipart_formdata(files): class HTTPTests(helper.CPWebCase): - def make_connection(self): if self.scheme == 'https': return HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) @@ -77,7 +79,6 @@ def make_connection(self): @staticmethod def setup_server(): class Root: - @cherrypy.expose def index(self, *args, **kwargs): return 'Hello world!' @@ -146,10 +147,11 @@ def test_no_content_length(self): c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) with mock.patch.object( - c, - '_get_content_length', - lambda body, method: None, - create=True): + c, + '_get_content_length', + lambda body, method: None, + create=True, + ): c.request('POST', '/') response = c.getresponse() @@ -199,8 +201,13 @@ def test_post_filename_with_special_characters(self): """ # We'll upload a bunch of files with differing names. fnames = [ - 'boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv', - 'file;name.csv', 'file; name.csv', u'test_łóąä.txt', + 'boop.csv', + 'foo, bar.csv', + 'bar, xxxx.csv', + 'file"name.csv', + 'file;name.csv', + 'file; name.csv', + 'test_łóąä.txt', ] for fname in fnames: files = [('myfile', fname, 'yunyeenyunyue')] @@ -240,8 +247,8 @@ def test_malformed_request_line(self): def test_request_line_split_issue_1220(self): params = { - 'intervenant-entreprise-evenement_classaction': - 'evenement-mailremerciements', + 'intervenant-entreprise-evenement_classaction': 'evenement-' + 'mailremerciements', '_path': 'intervenant-entreprise-evenement', 'intervenant-entreprise-evenement_action-id': 19404, 'intervenant-entreprise-evenement_id': 19404, @@ -283,8 +290,10 @@ def test_http_over_https(self): response.begin() self.assertEqual(response.status, 400) self.body = response.read() - self.assertBody('The client sent a plain HTTP request, but this ' - 'server only speaks HTTPS on this port.') + self.assertBody( + 'The client sent a plain HTTP request, but this ' + 'server only speaks HTTPS on this port.', + ) except socket.error: e = sys.exc_info()[1] # "Connection reset by peer" is also acceptable. @@ -300,8 +309,7 @@ def test_garbage_in(self): try: response.begin() self.assertEqual(response.status, 400) - self.assertEqual(response.fp.read(22), - b'Malformed Request-Line') + self.assertEqual(response.fp.read(22), b'Malformed Request-Line') c.close() except socket.error: e = sys.exc_info()[1] diff --git a/cherrypy/test/test_httputil.py b/cherrypy/test/test_httputil.py index 846614247..9adae2b56 100644 --- a/cherrypy/test/test_httputil.py +++ b/cherrypy/test/test_httputil.py @@ -1,4 +1,5 @@ """Test helpers from ``cherrypy.lib.httputil`` module.""" + import pytest import http.client @@ -24,7 +25,7 @@ ('', '/pi', '/pi'), ('', '/', '/'), ('', '', '/'), - ] + ], ) def test_urljoin(script_name, path_info, expected_url): """Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO.""" @@ -51,7 +52,7 @@ def test_urljoin(script_name, path_info, expected_url): ('500', EXPECTED_500), (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 @@ -64,7 +65,7 @@ def test_valid_status(status, expected_status): [ ( 'hey', - r"Illegal response status from server \('hey' is non-numeric\)." + r"Illegal response status from server \('hey' is non-numeric\).", ), ( {'hey': 'hi'}, @@ -73,7 +74,7 @@ def test_valid_status(status, expected_status): ), (1, r'Illegal response status from server \(1 is out of range\).'), (600, r'Illegal response status from server \(600 is out of range\).'), - ] + ], ) def test_invalid_status(status_code, error_msg): """Check that invalid status cause certain errors.""" diff --git a/cherrypy/test/test_iterator.py b/cherrypy/test/test_iterator.py index 5bad59be1..ffcca536d 100644 --- a/cherrypy/test/test_iterator.py +++ b/cherrypy/test/test_iterator.py @@ -3,7 +3,6 @@ class IteratorBase(object): - created = 0 datachunk = 'butternut squash' * 256 @@ -17,7 +16,6 @@ def decr(cls): class OurGenerator(IteratorBase): - def __iter__(self): self.incr() try: @@ -28,7 +26,6 @@ def __iter__(self): class OurIterator(IteratorBase): - started = False closed_off = False count = 0 @@ -60,13 +57,11 @@ def __del__(self): class OurClosableIterator(OurIterator): - def close(self): self.decrement() class OurNotClosableIterator(OurIterator): - # We can't close something which requires an additional argument. def close(self, somearg): self.decrement() @@ -77,12 +72,9 @@ class OurUnclosableIterator(OurIterator): class IteratorTest(helper.CPWebCase): - @staticmethod def setup_server(): - class Root(object): - @cherrypy.expose def count(self, clsname): cherrypy.response.headers['Content-Type'] = 'text/plain' @@ -118,6 +110,7 @@ def _test_iterator(self): all_classes = closables + unclosables import random + random.shuffle(all_classes) for clsname in all_classes: @@ -176,12 +169,12 @@ def _test_iterator(self): # we will test to see if the value has gone back down to # zero. if clsname in closables: - # Sometimes we try to get the answer too quickly - we # will wait for 100 ms before asking again if we didn't # get the answer we wanted. if self.body != '0': import time + time.sleep(0.1) self.getPage('/count/' + clsname) diff --git a/cherrypy/test/test_json.py b/cherrypy/test/test_json.py index 4b8be548f..0878b740f 100644 --- a/cherrypy/test/test_json.py +++ b/cherrypy/test/test_json.py @@ -8,11 +8,9 @@ class JsonTest(helper.CPWebCase): - @staticmethod def setup_server(): class Root(object): - @cherrypy.expose def plain(self): return 'hello' @@ -72,20 +70,26 @@ def test_json_input(self): return body = '[13, "c"]' - headers = [('Content-Type', 'application/json'), - ('Content-Length', str(len(body)))] + headers = [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(body))), + ] self.getPage('/json_post', method='POST', headers=headers, body=body) self.assertBody('ok') body = '[13, "c"]' - headers = [('Content-Type', 'text/plain'), - ('Content-Length', str(len(body)))] + headers = [ + ('Content-Type', 'text/plain'), + ('Content-Length', str(len(body))), + ] self.getPage('/json_post', method='POST', headers=headers, body=body) self.assertStatus(415, 'Expected an application/json content type') body = '[13, -]' - headers = [('Content-Type', 'application/json'), - ('Content-Length', str(len(body)))] + headers = [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(body))), + ] self.getPage('/json_post', method='POST', headers=headers, body=body) self.assertStatus(400, 'Invalid JSON document') diff --git a/cherrypy/test/test_logging.py b/cherrypy/test/test_logging.py index 052039265..7b4729209 100644 --- a/cherrypy/test/test_logging.py +++ b/cherrypy/test/test_logging.py @@ -12,8 +12,8 @@ # Some unicode strings. -tartaros = u'\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2' -erebos = u'\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com' +tartaros = '\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2' +erebos = '\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com' @pytest.fixture @@ -48,7 +48,6 @@ def shutdown_server(): @pytest.fixture def configure_server(access_log_file, error_log_file): class Root: - @cherrypy.expose def index(self): return 'hello' @@ -87,16 +86,20 @@ def error(self): root = Root() cherrypy.config.reset() - cherrypy.config.update({ - 'server.socket_host': webtest.WebCase.HOST, - 'server.socket_port': webtest.WebCase.PORT, - 'server.protocol_version': webtest.WebCase.PROTOCOL, - 'environment': 'test_suite', - }) - cherrypy.config.update({ - 'log.error_file': str(error_log_file), - 'log.access_file': str(access_log_file), - }) + cherrypy.config.update( + { + 'server.socket_host': webtest.WebCase.HOST, + 'server.socket_port': webtest.WebCase.PORT, + 'server.protocol_version': webtest.WebCase.PROTOCOL, + 'environment': 'test_suite', + }, + ) + cherrypy.config.update( + { + 'log.error_file': str(error_log_file), + 'log.access_file': str(access_log_file), + }, + ) cherrypy.tree.mount(root) @@ -104,6 +107,7 @@ def error(self): def log_tracker(access_log_file): class LogTracker(LogCase): logfile = str(access_log_file) + return LogTracker() @@ -128,16 +132,14 @@ def test_normal_return(log_tracker, server): content_length = len(expected_body) if not any( - k for k, v in resp.headers.items() - if k.lower() == 'content-length' + k for k, v in resp.headers.items() if k.lower() == 'content-length' ): content_length = '-' log_tracker.assertLog( -1, '] "GET /as_string HTTP/1.1" 200 %s ' - '"http://www.cherrypy.dev/" "Mozilla/5.0"' - % content_length, + '"http://www.cherrypy.dev/" "Mozilla/5.0"' % content_length, ) @@ -160,15 +162,13 @@ def test_normal_yield(log_tracker, server): log_tracker.assertLog(-1, intro) content_length = len(expected_body) if not any( - k for k, v in resp.headers.items() - if k.lower() == 'content-length' + k for k, v in resp.headers.items() if k.lower() == 'content-length' ): content_length = '-' log_tracker.assertLog( -1, - '] "GET /as_yield HTTP/1.1" 200 %s "" ""' - % content_length, + '] "GET /as_yield HTTP/1.1" 200 %s "" ""' % content_length, ) @@ -193,8 +193,7 @@ def test_custom_log_format(log_tracker, monkeypatch, server): log_tracker.assertLog(-1, '%s - - [' % host) log_tracker.assertLog( -1, - '] "GET /as_string HTTP/1.1" ' - '200 7 "REFERER" "USERAGENT" HOST', + '] "GET /as_string HTTP/1.1" 200 7 "REFERER" "USERAGENT" HOST', ) @@ -254,8 +253,7 @@ def test_timez_log_format(log_tracker, monkeypatch, server): log_tracker.assertLog(-1, expected_time) log_tracker.assertLog( -1, - ' "GET /as_string HTTP/1.1" ' - '200 7 "REFERER" "USERAGENT" HOST', + ' "GET /as_string HTTP/1.1" 200 7 "REFERER" "USERAGENT" HOST', ) diff --git a/cherrypy/test/test_mime.py b/cherrypy/test/test_mime.py index ef35d10e7..0828006a3 100644 --- a/cherrypy/test/test_mime.py +++ b/cherrypy/test/test_mime.py @@ -6,9 +6,7 @@ def setup_server(): - class Root: - @cherrypy.expose def multipart(self, parts): return repr(parts) @@ -19,8 +17,11 @@ def multipart_form_data(self, **kwargs): @cherrypy.expose def flashupload(self, Filedata, Upload, Filename): - return ('Upload: %s, Filename: %s, Filedata: %r' % - (Upload, Filename, Filedata.file.read())) + return 'Upload: %s, Filename: %s, Filedata: %r' % ( + Upload, + Filename, + Filedata.file.read(), + ) cherrypy.config.update({'server.max_request_body_size': 0}) cherrypy.tree.mount(Root()) @@ -45,18 +46,22 @@ def test_multipart(self): This is the HTML version -""") - body = '\r\n'.join([ - '--123456789', - "Content-Type: text/plain; charset='ISO-8859-1'", - 'Content-Transfer-Encoding: 7bit', - '', - text_part, - '--123456789', - "Content-Type: text/html; charset='ISO-8859-1'", - '', - html_part, - '--123456789--']) +""", + ) + body = '\r\n'.join( + [ + '--123456789', + "Content-Type: text/plain; charset='ISO-8859-1'", + 'Content-Transfer-Encoding: 7bit', + '', + text_part, + '--123456789', + "Content-Type: text/html; charset='ISO-8859-1'", + '', + html_part, + '--123456789--', + ], + ) headers = [ ('Content-Type', 'multipart/mixed; boundary=123456789'), ('Content-Length', str(len(body))), @@ -65,32 +70,40 @@ def test_multipart(self): self.assertBody(repr([text_part, html_part])) def test_multipart_form_data(self): - body = '\r\n'.join([ - '--X', - 'Content-Disposition: form-data; name="foo"', - '', - 'bar', - '--X', - # Test a param with more than one value. - # See - # https://github.com/cherrypy/cherrypy/issues/1028 - 'Content-Disposition: form-data; name="baz"', - '', - '111', - '--X', - 'Content-Disposition: form-data; name="baz"', - '', - '333', - '--X--' - ]) - self.getPage('/multipart_form_data', method='POST', - headers=[( - 'Content-Type', 'multipart/form-data;boundary=X'), - ('Content-Length', str(len(body))), - ], - body=body), + body = '\r\n'.join( + [ + '--X', + 'Content-Disposition: form-data; name="foo"', + '', + 'bar', + '--X', + # Test a param with more than one value. + # See + # https://github.com/cherrypy/cherrypy/issues/1028 + 'Content-Disposition: form-data; name="baz"', + '', + '111', + '--X', + 'Content-Disposition: form-data; name="baz"', + '', + '333', + '--X--', + ], + ) + ( + self.getPage( + '/multipart_form_data', + method='POST', + headers=[ + ('Content-Type', 'multipart/form-data;boundary=X'), + ('Content-Length', str(len(body))), + ], + body=body, + ), + ) self.assertBody( - repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))])) + repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))]), + ) class SafeMultipartHandlingTest(helper.CPWebCase): @@ -99,17 +112,22 @@ class SafeMultipartHandlingTest(helper.CPWebCase): def test_Flash_Upload(self): headers = [ ('Accept', 'text/*'), - ('Content-Type', 'multipart/form-data; ' - 'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'), + ( + 'Content-Type', + 'multipart/form-data; ' + 'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6', + ), ('User-Agent', 'Shockwave Flash'), ('Host', 'www.example.com:54583'), ('Content-Length', '499'), ('Connection', 'Keep-Alive'), ('Cache-Control', 'no-cache'), ] - filedata = (b'\r\n' - b'\r\n' - b'\r\n') + filedata = ( + b'\r\n' + b'\r\n' + b'\r\n' + ) body = ( b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' b'Content-Disposition: form-data; name="Filename"\r\n' @@ -119,9 +137,7 @@ def test_Flash_Upload(self): b'Content-Disposition: form-data; ' b'name="Filedata"; filename=".project"\r\n' b'Content-Type: application/octet-stream\r\n' - b'\r\n' + - filedata + - b'\r\n' + b'\r\n' + filedata + b'\r\n' b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' b'Content-Disposition: form-data; name="Upload"\r\n' b'\r\n' @@ -130,5 +146,7 @@ def test_Flash_Upload(self): b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--' ) self.getPage('/flashupload', headers, 'POST', body) - self.assertBody('Upload: Submit Query, Filename: .project, ' - 'Filedata: %r' % filedata) + self.assertBody( + 'Upload: Submit Query, Filename: .project, Filedata: %r' + % filedata, + ) diff --git a/cherrypy/test/test_misc_tools.py b/cherrypy/test/test_misc_tools.py index fb85b8f89..df132ba6f 100644 --- a/cherrypy/test/test_misc_tools.py +++ b/cherrypy/test/test_misc_tools.py @@ -11,28 +11,29 @@ def setup_server(): class Root: - @cherrypy.expose def index(self): yield 'Hello, world' + h = [('Content-Language', 'en-GB'), ('Content-Type', 'text/plain')] tools.response_headers(headers=h)(index) @cherrypy.expose - @cherrypy.config(**{ - 'tools.response_headers.on': True, - 'tools.response_headers.headers': [ - ('Content-Language', 'fr'), - ('Content-Type', 'text/plain'), - ], - 'tools.log_hooks.on': True, - }) + @cherrypy.config( + **{ + 'tools.response_headers.on': True, + 'tools.response_headers.headers': [ + ('Content-Language', 'fr'), + ('Content-Type', 'text/plain'), + ], + 'tools.log_hooks.on': True, + }, + ) def other(self): return 'salut' @cherrypy.config(**{'tools.accept.on': True}) class Accept: - @cherrypy.expose def index(self): return 'Atom feed' @@ -55,14 +56,13 @@ def select(self): return 'PAGE TITLE' class Referer: - @cherrypy.expose def accept(self): return 'Accepted!' + reject = accept class AutoVary: - @cherrypy.expose def index(self): # Read a header directly with 'get' @@ -77,14 +77,17 @@ def index(self): tools.accept.callable(['text/html', 'text/plain']) return 'Hello, world!' - conf = {'/referer': {'tools.referer.on': True, - 'tools.referer.pattern': r'http://[^/]*example\.com', - }, - '/referer/reject': {'tools.referer.accept': False, - 'tools.referer.accept_missing': True, - }, - '/autovary': {'tools.autovary.on': True}, - } + conf = { + '/referer': { + 'tools.referer.on': True, + 'tools.referer.pattern': r'http://[^/]*example\.com', + }, + '/referer/reject': { + 'tools.referer.accept': False, + 'tools.referer.accept_missing': True, + }, + '/autovary': {'tools.autovary.on': True}, + } root = Root() root.referer = Referer() @@ -115,8 +118,10 @@ def testReferer(self): self.getPage('/referer/accept') self.assertErrorPage(403, 'Forbidden Referer header.') - self.getPage('/referer/accept', - headers=[('Referer', 'http://www.example.com/')]) + self.getPage( + '/referer/accept', + headers=[('Referer', 'http://www.example.com/')], + ) self.assertStatus(200) self.assertBody('Accepted!') @@ -125,8 +130,10 @@ def testReferer(self): self.assertStatus(200) self.assertBody('Accepted!') - self.getPage('/referer/reject', - headers=[('Referer', 'http://www.example.com/')]) + self.getPage( + '/referer/reject', + headers=[('Referer', 'http://www.example.com/')], + ) self.assertErrorPage(403, 'Forbidden Referer header.') @@ -140,8 +147,10 @@ def test_Accept_Tool(self): self.assertInBody('Unknown Blog') # Specify exact media type - self.getPage('/accept/feed', - headers=[('Accept', 'application/atom+xml')]) + self.getPage( + '/accept/feed', + headers=[('Accept', 'application/atom+xml')], + ) self.assertStatus(200) self.assertInBody('Unknown Blog') @@ -157,10 +166,12 @@ def test_Accept_Tool(self): # Specify unacceptable media types self.getPage('/accept/feed', headers=[('Accept', 'text/html')]) - self.assertErrorPage(406, - 'Your client sent this Accept header: text/html. ' - 'But this resource only emits these media types: ' - 'application/atom+xml.') + self.assertErrorPage( + 406, + 'Your client sent this Accept header: text/html. ' + 'But this resource only emits these media types: ' + 'application/atom+xml.', + ) # Test resource where tool is 'on' but media is None (not set). self.getPage('/accept/') @@ -175,8 +186,10 @@ def test_accept_selection(self): self.getPage('/accept/select', [('Accept', 'text/plain')]) self.assertStatus(200) self.assertBody('PAGE TITLE') - self.getPage('/accept/select', - [('Accept', 'text/plain, text/*;q=0.5')]) + self.getPage( + '/accept/select', + [('Accept', 'text/plain, text/*;q=0.5')], + ) self.assertStatus(200) self.assertBody('PAGE TITLE') @@ -195,7 +208,8 @@ def test_accept_selection(self): 406, 'Your client sent this Accept header: application/xml. ' 'But this resource only emits these media types: ' - 'text/html, text/plain.') + 'text/html, text/plain.', + ) class AutoVaryTest(helper.CPWebCase): @@ -206,5 +220,5 @@ def testAutoVary(self): self.assertHeader( 'Vary', 'Accept, Accept-Charset, Accept-Encoding, ' - 'Host, If-Modified-Since, Range' + 'Host, If-Modified-Since, Range', ) diff --git a/cherrypy/test/test_native.py b/cherrypy/test/test_native.py index 08bf99977..71b6010e4 100644 --- a/cherrypy/test/test_native.py +++ b/cherrypy/test/test_native.py @@ -15,6 +15,7 @@ @pytest.fixture def cp_native_server(request): """A native server.""" + class Root(object): @cherrypy.expose def index(self): diff --git a/cherrypy/test/test_objectmapping.py b/cherrypy/test/test_objectmapping.py index 98402b8b9..9afa0fddc 100644 --- a/cherrypy/test/test_objectmapping.py +++ b/cherrypy/test/test_objectmapping.py @@ -8,11 +8,9 @@ class ObjectMappingTest(helper.CPWebCase): - @staticmethod def setup_server(): class Root: - @cherrypy.expose def index(self, name='world'): return name @@ -55,26 +53,26 @@ def translate_html(self): @cherrypy.expose def mapped_func(self, ID=None): return 'ID is %s' % ID + setattr(Root, 'Von B\xfclow', mapped_func) class Exposing: - @cherrypy.expose def base(self): return 'expose works!' + cherrypy.expose(base, '1') cherrypy.expose(base, '2') class ExposingNewStyle(object): - @cherrypy.expose def base(self): return 'expose works!' + cherrypy.expose(base, '1') cherrypy.expose(base, '2') class Dir1: - @cherrypy.expose def index(self): return 'index for dir1' @@ -83,14 +81,14 @@ def index(self): @cherrypy.config(**{'tools.trailing_slash.extra': True}) def myMethod(self): return 'myMethod from dir1, path_info is:' + repr( - cherrypy.request.path_info) + cherrypy.request.path_info, + ) @cherrypy.expose def default(self, *params): return 'default for dir1, param is:' + repr(params) class Dir2: - @cherrypy.expose def index(self): return 'index for dir2, path is:' + cherrypy.request.path_info @@ -108,17 +106,14 @@ def posparam(self, *vpath): return '/'.join(vpath) class Dir3: - def default(self): return 'default for dir3, not exposed' class Dir4: - def index(self): return 'index for dir4, not exposed' class DefNoIndex: - @cherrypy.expose def default(self, *args): raise cherrypy.HTTPRedirect('contact') @@ -126,7 +121,6 @@ def default(self, *args): # MethodDispatcher code @cherrypy.expose class ByMethod: - def __init__(self, *things): self.things = list(things) @@ -151,14 +145,14 @@ class Collection: d = cherrypy.dispatch.MethodDispatcher() for url in script_names: - conf = {'/': {'user': (url or '/').split('/')[-2]}, - '/bymethod': {'request.dispatch': d}, - '/collection': {'request.dispatch': d}, - } + conf = { + '/': {'user': (url or '/').split('/')[-2]}, + '/bymethod': {'request.dispatch': d}, + '/collection': {'request.dispatch': d}, + } cherrypy.tree.mount(Root(), url, conf) class Isolated: - @cherrypy.expose def index(self): return 'made it!' @@ -167,12 +161,14 @@ def index(self): @cherrypy.expose class AnotherApp: - def GET(self): return 'milk' - cherrypy.tree.mount(AnotherApp(), '/app', - {'/': {'request.dispatch': d}}) + cherrypy.tree.mount( + AnotherApp(), + '/app', + {'/': {'request.dispatch': d}}, + ) def testObjectMapping(self): for url in script_names: @@ -183,11 +179,13 @@ def testObjectMapping(self): self.getPage('/dir1/myMethod') self.assertBody( - "myMethod from dir1, path_info is:'/dir1/myMethod'") + "myMethod from dir1, path_info is:'/dir1/myMethod'", + ) self.getPage('/this/method/does/not/exist') self.assertBody( - "default:('this', 'method', 'does', 'not', 'exist')") + "default:('this', 'method', 'does', 'not', 'exist')", + ) self.getPage('/extra/too/much') self.assertBody("('too', 'much')") @@ -214,7 +212,8 @@ def testObjectMapping(self): # Test that default method must be exposed in order to match. self.getPage('/dir1/dir2/dir3/dir4/index') self.assertBody( - "default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')") + "default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')", + ) # Test *vpath when default() is defined but not index() # This also tests HTTPRedirect with default. @@ -223,12 +222,16 @@ def testObjectMapping(self): self.assertHeader('Location', '%s/contact' % self.base()) self.getPage('/defnoindex/') self.assertStatus((302, 303)) - self.assertHeader('Location', '%s/defnoindex/contact' % - self.base()) + self.assertHeader( + 'Location', + '%s/defnoindex/contact' % self.base(), + ) self.getPage('/defnoindex/page') self.assertStatus((302, 303)) - self.assertHeader('Location', '%s/defnoindex/contact' % - self.base()) + self.assertHeader( + 'Location', + '%s/defnoindex/contact' % self.base(), + ) self.getPage('/redirect') self.assertStatus('302 Found') @@ -262,8 +265,10 @@ def testObjectMapping(self): self.getPage('http://%s:%s/' % (self.interface(), self.PORT)) self.assertBody('world') - self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' % - (self.interface(), self.PORT)) + self.getPage( + 'http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' + % (self.interface(), self.PORT), + ) self.assertBody("default:('abs',)") self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z') @@ -387,7 +392,6 @@ def testMethodDispatch(self): def testTreeMounting(self): class Root(object): - @cherrypy.expose def hello(self): return 'Hello world!' diff --git a/cherrypy/test/test_params.py b/cherrypy/test/test_params.py index 73b4cb4cf..62c406c27 100644 --- a/cherrypy/test/test_params.py +++ b/cherrypy/test/test_params.py @@ -14,8 +14,10 @@ class Root: @cherrypy.tools.params() def resource(self, limit=None, sort=None): return type(limit).__name__ + # for testing on Py 2 resource.__annotations__ = {'limit': int} + conf = {'/': {'tools.params.on': True}} cherrypy.tree.mount(Root(), config=conf) diff --git a/cherrypy/test/test_plugins.py b/cherrypy/test/test_plugins.py index e69212db5..189e9a0fa 100644 --- a/cherrypy/test/test_plugins.py +++ b/cherrypy/test/test_plugins.py @@ -7,6 +7,7 @@ class TestAutoreloader: def test_file_for_file_module_when_None(self): """No error when ``module.__file__`` is :py:data:`None`.""" + class test_module: __file__ = None diff --git a/cherrypy/test/test_proxy.py b/cherrypy/test/test_proxy.py index 4d34440ab..03a2b0600 100644 --- a/cherrypy/test/test_proxy.py +++ b/cherrypy/test/test_proxy.py @@ -5,24 +5,25 @@ class ProxyTest(helper.CPWebCase): - @staticmethod def setup_server(): - # Set up site - cherrypy.config.update({ - 'tools.proxy.on': True, - 'tools.proxy.base': 'www.mydomain.test', - }) + cherrypy.config.update( + { + 'tools.proxy.on': True, + 'tools.proxy.base': 'www.mydomain.test', + }, + ) # Set up application class Root: - def __init__(self, sn): # Calculate a URL outside of any requests. self.thisnewpage = cherrypy.url( - '/this/new/page', script_name=sn) + '/this/new/page', + script_name=sn, + ) @cherrypy.expose def pageurl(self): @@ -37,10 +38,12 @@ def remoteip(self): return cherrypy.request.remote.ip @cherrypy.expose - @cherrypy.config(**{ - 'tools.proxy.local': 'X-Host', - 'tools.trailing_slash.extra': True, - }) + @cherrypy.config( + **{ + 'tools.proxy.local': 'X-Host', + 'tools.trailing_slash.extra': True, + }, + ) def xhost(self): raise cherrypy.HTTPRedirect('blah') @@ -55,13 +58,16 @@ def ssl(self): @cherrypy.expose def newurl(self): - return ("Browse to this page." - % cherrypy.url('/this/new/page')) + return "Browse to this page." % cherrypy.url( + '/this/new/page', + ) @cherrypy.expose - @cherrypy.config(**{ - 'tools.proxy.base': None, - }) + @cherrypy.config( + **{ + 'tools.proxy.base': None, + }, + ) def base_no_base(self): return cherrypy.request.base @@ -70,38 +76,53 @@ def base_no_base(self): def testProxy(self): self.getPage('/') - self.assertHeader('Location', - '%s://www.mydomain.test%s/dummy' % - (self.scheme, self.prefix())) + self.assertHeader( + 'Location', + '%s://www.mydomain.test%s/dummy' % (self.scheme, self.prefix()), + ) # Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2) self.getPage( - '/', headers=[('X-Forwarded-Host', 'http://www.example.test')]) + '/', + headers=[('X-Forwarded-Host', 'http://www.example.test')], + ) self.assertHeader('Location', 'http://www.example.test/dummy') self.getPage('/', headers=[('X-Forwarded-Host', 'www.example.test')]) - self.assertHeader('Location', '%s://www.example.test/dummy' % - self.scheme) + self.assertHeader( + 'Location', + '%s://www.example.test/dummy' % self.scheme, + ) # Test multiple X-Forwarded-Host headers - self.getPage('/', headers=[ - ('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'), - ]) + self.getPage( + '/', + headers=[ + ( + 'X-Forwarded-Host', + 'http://www.example.test, www.cherrypy.test', + ), + ], + ) self.assertHeader('Location', 'http://www.example.test/dummy') # Test X-Forwarded-For (Apache2) - self.getPage('/remoteip', - headers=[('X-Forwarded-For', '192.168.0.20')]) + self.getPage( + '/remoteip', + headers=[('X-Forwarded-For', '192.168.0.20')], + ) self.assertBody('192.168.0.20') # Fix bug #1268 - self.getPage('/remoteip', - headers=[ - ('X-Forwarded-For', '67.15.36.43, 192.168.0.20') - ]) + self.getPage( + '/remoteip', + headers=[('X-Forwarded-For', '67.15.36.43, 192.168.0.20')], + ) self.assertBody('67.15.36.43') # Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418) self.getPage('/xhost', headers=[('X-Host', 'www.example.test')]) - self.assertHeader('Location', '%s://www.example.test/blah' % - self.scheme) + self.assertHeader( + 'Location', + '%s://www.example.test/blah' % self.scheme, + ) # Test X-Forwarded-Proto (lighttpd) self.getPage('/base', headers=[('X-Forwarded-Proto', 'https')]) @@ -116,12 +137,19 @@ def testProxy(self): # Test the value inside requests self.getPage(sn + '/newurl') self.assertBody( - "Browse to this page.") - self.getPage(sn + '/newurl', headers=[('X-Forwarded-Host', - 'http://www.example.test')]) - self.assertBody("Browse to this page.") + "Browse to this page.", + ) + self.getPage( + sn + '/newurl', + headers=[('X-Forwarded-Host', 'http://www.example.test')], + ) + self.assertBody( + "Browse to this page.", + ) # Test the value outside requests port = '' @@ -132,17 +160,24 @@ def testProxy(self): host = self.HOST if host in ('0.0.0.0', '::'): import socket + host = socket.gethostname() - expected = ('%s://%s%s%s/this/new/page' - % (self.scheme, host, port, sn)) + expected = '%s://%s%s%s/this/new/page' % ( + self.scheme, + host, + port, + sn, + ) self.getPage(sn + '/pageurl') self.assertBody(expected) # Test trailing slash (see # https://github.com/cherrypy/cherrypy/issues/562). self.getPage('/xhost/', headers=[('X-Host', 'www.example.test')]) - self.assertHeader('Location', '%s://www.example.test/xhost' - % self.scheme) + self.assertHeader( + 'Location', + '%s://www.example.test/xhost' % self.scheme, + ) def test_no_base_port_in_host(self): """ diff --git a/cherrypy/test/test_refleaks.py b/cherrypy/test/test_refleaks.py index b5030a596..1aad30096 100644 --- a/cherrypy/test/test_refleaks.py +++ b/cherrypy/test/test_refleaks.py @@ -16,12 +16,9 @@ class ReferenceTests(helper.CPWebCase): - @staticmethod def setup_server(): - class Root: - @cherrypy.expose def index(self, *args, **kwargs): cherrypy.request.thing = data @@ -55,10 +52,7 @@ def getpage(): ITERATIONS = 25 - ts = [ - threading.Thread(target=getpage) - for _ in range(ITERATIONS) - ] + ts = [threading.Thread(target=getpage) for _ in range(ITERATIONS)] for t in ts: t.start() diff --git a/cherrypy/test/test_request_obj.py b/cherrypy/test/test_request_obj.py index 2b3f60f0d..8c279a6d0 100644 --- a/cherrypy/test/test_request_obj.py +++ b/cherrypy/test/test_request_obj.py @@ -14,19 +14,26 @@ localDir = os.path.dirname(__file__) -defined_http_methods = ('OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', - 'TRACE', 'PROPFIND', 'PATCH') +defined_http_methods = ( + 'OPTIONS', + 'GET', + 'HEAD', + 'POST', + 'PUT', + 'DELETE', + 'TRACE', + 'PROPFIND', + 'PATCH', +) # Client-side code # class RequestObjectTests(helper.CPWebCase): - @staticmethod def setup_server(): class Root: - @cherrypy.expose def index(self): return 'hello' @@ -64,21 +71,21 @@ class TestType(type): subclass, and adds an instance of the subclass as an attribute of root. """ + def __init__(cls, name, bases, dct): type.__init__(cls, name, bases, dct) for value in dct.values(): if isinstance(value, types.FunctionType): value.exposed = True setattr(root, name.lower(), cls()) + Test = TestType('Test', (object,), {}) class PathInfo(Test): - def default(self, *args): return cherrypy.request.path_info class Params(Test): - def index(self, thing): return repr(thing) @@ -91,7 +98,6 @@ def default(self, *args, **kwargs): @cherrypy.expose class ParamErrorsCallable(object): - def __call__(self): return 'data' @@ -99,10 +105,10 @@ def handler_dec(f): @wraps(f) def wrapper(handler, *args, **kwargs): return f(handler, *args, **kwargs) + return wrapper class ParamErrors(Test): - @cherrypy.expose def one_positional(self, param1): return 'data' @@ -152,25 +158,34 @@ def raise_type_error_decorated(self, *args, **kwargs): def callable_error_page(status, **kwargs): return "Error %s - Well, I'm very sorry but you haven't paid!" % ( - status) + status + ) @cherrypy.config(**{'tools.log_tracebacks.on': True}) class Error(Test): - def reason_phrase(self): raise cherrypy.HTTPError("410 Gone fishin'") - @cherrypy.config(**{ - 'error_page.404': os.path.join(localDir, 'static/index.html'), - 'error_page.401': callable_error_page, - }) + @cherrypy.config( + **{ + 'error_page.404': os.path.join( + localDir, + 'static/index.html', + ), + 'error_page.401': callable_error_page, + }, + ) def custom(self, err='404'): raise cherrypy.HTTPError( - int(err), 'No, really, not found!') + int(err), + 'No, really, not found!', + ) - @cherrypy.config(**{ - 'error_page.default': callable_error_page, - }) + @cherrypy.config( + **{ + 'error_page.default': callable_error_page, + }, + ) def custom_default(self): return 1 + 'a' # raise an unexpected error @@ -204,7 +219,6 @@ def rethrow(self): raise ValueError() class Expect(Test): - def expectation_failed(self): expect = cherrypy.request.headers.elements('Expect') if expect and expect[0].value != '100-continue': @@ -212,7 +226,6 @@ def expectation_failed(self): raise cherrypy.HTTPError(417, 'Expectation Failed') class Headers(Test): - def default(self, headername): """Spit back out the value for the requested header.""" return cherrypy.request.headers[headername] @@ -229,10 +242,11 @@ def doubledheaders(self): hMap['content-type'] = 'text/html' hMap['content-length'] = 18 hMap['server'] = 'CherryPy headertest' - hMap['location'] = ('%s://%s:%s/headers/' - % (cherrypy.request.local.ip, - cherrypy.request.local.port, - cherrypy.request.scheme)) + hMap['location'] = '%s://%s:%s/headers/' % ( + cherrypy.request.local.ip, + cherrypy.request.local.port, + cherrypy.request.scheme, + ) # Set a rare header for fun hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT' @@ -246,13 +260,11 @@ def ifmatch(self): return val class HeaderElements(Test): - def get_elements(self, headername): e = cherrypy.request.headers.elements(headername) return '\n'.join([str(x) for x in e]) class Method(Test): - def index(self): m = cherrypy.request.method if m in defined_http_methods or m == 'CONNECT': @@ -294,16 +306,18 @@ def index(self): for id, contents in self.documents.items(): yield ( "

  • %s:" - ' %s
  • \n' % (id, id, contents)) + ' %s\n' % (id, id, contents) + ) yield '
' @cherrypy.expose def get(self, ID): - return ('Divorce document %s: %s' % - (ID, self.documents.get(ID, 'empty'))) + return 'Divorce document %s: %s' % ( + ID, + self.documents.get(ID, 'empty'), + ) class ThreadLocal(Test): - def index(self): existing = repr(getattr(cherrypy.request, 'asdf', None)) cherrypy.request.asdf = 'rassfrassin' @@ -311,8 +325,12 @@ def index(self): appconf = { '/method': { - 'request.methods_with_bodies': ('POST', 'PUT', 'PROPFIND', - 'PATCH') + 'request.methods_with_bodies': ( + 'POST', + 'PUT', + 'PROPFIND', + 'PATCH', + ), }, } cherrypy.tree.mount(root, config=appconf) @@ -324,16 +342,16 @@ def test_scheme(self): def test_per_request_uuid4(self): self.getPage('/request_uuid4') first_uuid4, _, second_uuid4 = self.body.decode().partition(' ') - assert ( - uuid.UUID(first_uuid4, version=4) - == uuid.UUID(second_uuid4, version=4) + assert uuid.UUID(first_uuid4, version=4) == uuid.UUID( + second_uuid4, + version=4, ) self.getPage('/request_uuid4') third_uuid4, _, _ = self.body.decode().partition(' ') - assert ( - uuid.UUID(first_uuid4, version=4) - != uuid.UUID(third_uuid4, version=4) + assert uuid.UUID(first_uuid4, version=4) != uuid.UUID( + third_uuid4, + version=4, ) def testRelativeURIPathInfo(self): @@ -368,16 +386,25 @@ def testParams(self): # Test "% HEX HEX"-encoded URL, param keys, and values self.getPage('/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville') - self.assertBody('args: %s kwargs: %s' % - (('\xd4 \xe3', 'cheese'), - [('Gruy\xe8re', ntou('Bulgn\xe9ville'))])) + self.assertBody( + 'args: %s kwargs: %s' + % ( + ('\xd4 \xe3', 'cheese'), + [('Gruy\xe8re', ntou('Bulgn\xe9ville'))], + ), + ) # Make sure that encoded = and & get parsed correctly self.getPage( - '/params/code?url=http%3A//cherrypy.dev/index%3Fa%3D1%26b%3D2') - self.assertBody('args: %s kwargs: %s' % - (('code',), - [('url', ntou('http://cherrypy.dev/index?a=1&b=2'))])) + '/params/code?url=http%3A//cherrypy.dev/index%3Fa%3D1%26b%3D2', + ) + self.assertBody( + 'args: %s kwargs: %s' + % ( + ('code',), + [('url', ntou('http://cherrypy.dev/index?a=1&b=2'))], + ), + ) # Test coordinates sent by self.getPage('/params/ismap?223,114') @@ -385,40 +412,46 @@ def testParams(self): # Test "name[key]" dict-like params self.getPage('/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz') - self.assertBody('args: %s kwargs: %s' % - (('dictlike',), - [('a[1]', ntou('1')), ('a[2]', ntou('2')), - ('b', ntou('foo')), ('b[bar]', ntou('baz'))])) + self.assertBody( + 'args: %s kwargs: %s' + % ( + ('dictlike',), + [ + ('a[1]', ntou('1')), + ('a[2]', ntou('2')), + ('b', ntou('foo')), + ('b[bar]', ntou('baz')), + ], + ), + ) def testParamErrors(self): - # test that all of the handlers work when given # the correct parameters in order to ensure that the # errors below aren't coming from some other source. for uri in ( - '/paramerrors/one_positional?param1=foo', - '/paramerrors/one_positional_args?param1=foo', - '/paramerrors/one_positional_args/foo', - '/paramerrors/one_positional_args/foo/bar/baz', - '/paramerrors/one_positional_args_kwargs?' - 'param1=foo¶m2=bar', - '/paramerrors/one_positional_args_kwargs/foo?' - 'param2=bar¶m3=baz', - '/paramerrors/one_positional_args_kwargs/foo/bar/baz?' - 'param2=bar¶m3=baz', - '/paramerrors/one_positional_kwargs?' - 'param1=foo¶m2=bar¶m3=baz', - '/paramerrors/one_positional_kwargs/foo?' - 'param4=foo¶m2=bar¶m3=baz', - '/paramerrors/no_positional', - '/paramerrors/no_positional_args/foo', - '/paramerrors/no_positional_args/foo/bar/baz', - '/paramerrors/no_positional_args_kwargs?param1=foo¶m2=bar', - '/paramerrors/no_positional_args_kwargs/foo?param2=bar', - '/paramerrors/no_positional_args_kwargs/foo/bar/baz?' - 'param2=bar¶m3=baz', - '/paramerrors/no_positional_kwargs?param1=foo¶m2=bar', - '/paramerrors/callable_object', + '/paramerrors/one_positional?param1=foo', + '/paramerrors/one_positional_args?param1=foo', + '/paramerrors/one_positional_args/foo', + '/paramerrors/one_positional_args/foo/bar/baz', + '/paramerrors/one_positional_args_kwargs?param1=foo¶m2=bar', + '/paramerrors/one_positional_args_kwargs/foo?' + 'param2=bar¶m3=baz', + '/paramerrors/one_positional_args_kwargs/foo/bar/baz?' + 'param2=bar¶m3=baz', + '/paramerrors/one_positional_kwargs?' + 'param1=foo¶m2=bar¶m3=baz', + '/paramerrors/one_positional_kwargs/foo?' + 'param4=foo¶m2=bar¶m3=baz', + '/paramerrors/no_positional', + '/paramerrors/no_positional_args/foo', + '/paramerrors/no_positional_args/foo/bar/baz', + '/paramerrors/no_positional_args_kwargs?param1=foo¶m2=bar', + '/paramerrors/no_positional_args_kwargs/foo?param2=bar', + '/paramerrors/no_positional_args_kwargs/foo/bar/baz?' + 'param2=bar¶m3=baz', + '/paramerrors/no_positional_kwargs?param1=foo¶m2=bar', + '/paramerrors/callable_object', ): self.getPage(uri) self.assertStatus(200) @@ -449,29 +482,42 @@ def testParamErrors(self): ('/paramerrors/one_positional?foo=foo', error_msgs[0]), ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]), ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]), - ('/paramerrors/one_positional/foo?param1=foo¶m2=foo', - error_msgs[2]), - ('/paramerrors/one_positional_args/foo?param1=foo¶m2=foo', - error_msgs[2]), - ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo', - error_msgs[3]), - ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?' - 'param1=bar¶m3=baz', - error_msgs[2]), - ('/paramerrors/one_positional_kwargs/foo?' - 'param1=foo¶m2=bar¶m3=baz', - error_msgs[2]), + ( + '/paramerrors/one_positional/foo?param1=foo¶m2=foo', + error_msgs[2], + ), + ( + '/paramerrors/one_positional_args/foo?param1=foo¶m2=foo', + error_msgs[2], + ), + ( + '/paramerrors/one_positional_args/foo/bar/baz?param2=foo', + error_msgs[3], + ), + ( + '/paramerrors/one_positional_args_kwargs/foo/bar/baz?' + 'param1=bar¶m3=baz', + error_msgs[2], + ), + ( + '/paramerrors/one_positional_kwargs/foo?' + 'param1=foo¶m2=bar¶m3=baz', + error_msgs[2], + ), ('/paramerrors/no_positional/boo', error_msgs[1]), ('/paramerrors/no_positional?param1=foo', error_msgs[3]), ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]), - ('/paramerrors/no_positional_kwargs/boo?param1=foo', - error_msgs[1]), + ( + '/paramerrors/no_positional_kwargs/boo?param1=foo', + error_msgs[1], + ), ('/paramerrors/callable_object?param1=foo', error_msgs[3]), ('/paramerrors/callable_object/boo', error_msgs[1]), ): for show_mismatched_params in (True, False): cherrypy.config.update( - {'request.show_mismatched_params': show_mismatched_params}) + {'request.show_mismatched_params': show_mismatched_params}, + ) self.getPage(uri) self.assertStatus(404) if show_mismatched_params: @@ -481,26 +527,44 @@ def testParamErrors(self): # if body parameters are wrong, a 400 must be returned. for uri, body, msg in ( - ('/paramerrors/one_positional/foo', - 'param1=foo', error_msgs[2]), - ('/paramerrors/one_positional/foo', - 'param1=foo¶m2=foo', error_msgs[2]), - ('/paramerrors/one_positional_args/foo', - 'param1=foo¶m2=foo', error_msgs[2]), - ('/paramerrors/one_positional_args/foo/bar/baz', - 'param2=foo', error_msgs[4]), - ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', - 'param1=bar¶m3=baz', error_msgs[2]), - ('/paramerrors/one_positional_kwargs/foo', - 'param1=foo¶m2=bar¶m3=baz', error_msgs[2]), - ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]), - ('/paramerrors/no_positional_args/boo', - 'param1=foo', error_msgs[4]), - ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]), + ('/paramerrors/one_positional/foo', 'param1=foo', error_msgs[2]), + ( + '/paramerrors/one_positional/foo', + 'param1=foo¶m2=foo', + error_msgs[2], + ), + ( + '/paramerrors/one_positional_args/foo', + 'param1=foo¶m2=foo', + error_msgs[2], + ), + ( + '/paramerrors/one_positional_args/foo/bar/baz', + 'param2=foo', + error_msgs[4], + ), + ( + '/paramerrors/one_positional_args_kwargs/foo/bar/baz', + 'param1=bar¶m3=baz', + error_msgs[2], + ), + ( + '/paramerrors/one_positional_kwargs/foo', + 'param1=foo¶m2=bar¶m3=baz', + error_msgs[2], + ), + ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]), + ( + '/paramerrors/no_positional_args/boo', + 'param1=foo', + error_msgs[4], + ), + ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]), ): for show_mismatched_params in (True, False): cherrypy.config.update( - {'request.show_mismatched_params': show_mismatched_params}) + {'request.show_mismatched_params': show_mismatched_params}, + ) self.getPage(uri, method='POST', body=body) self.assertStatus(400) if show_mismatched_params: @@ -511,24 +575,46 @@ def testParamErrors(self): # even if body parameters are wrong, if we get the uri wrong, then # it's a 404 for uri, body, msg in ( - ('/paramerrors/one_positional?param2=foo', - 'param1=foo', error_msgs[3]), - ('/paramerrors/one_positional/foo/bar', - 'param2=foo', error_msgs[1]), - ('/paramerrors/one_positional_args/foo/bar?param2=foo', - 'param3=foo', error_msgs[3]), - ('/paramerrors/one_positional_kwargs/foo/bar', - 'param2=bar¶m3=baz', error_msgs[1]), - ('/paramerrors/no_positional?param1=foo', - 'param2=foo', error_msgs[3]), - ('/paramerrors/no_positional_args/boo?param2=foo', - 'param1=foo', error_msgs[3]), - ('/paramerrors/callable_object?param2=bar', - 'param1=foo', error_msgs[3]), + ( + '/paramerrors/one_positional?param2=foo', + 'param1=foo', + error_msgs[3], + ), + ( + '/paramerrors/one_positional/foo/bar', + 'param2=foo', + error_msgs[1], + ), + ( + '/paramerrors/one_positional_args/foo/bar?param2=foo', + 'param3=foo', + error_msgs[3], + ), + ( + '/paramerrors/one_positional_kwargs/foo/bar', + 'param2=bar¶m3=baz', + error_msgs[1], + ), + ( + '/paramerrors/no_positional?param1=foo', + 'param2=foo', + error_msgs[3], + ), + ( + '/paramerrors/no_positional_args/boo?param2=foo', + 'param1=foo', + error_msgs[3], + ), + ( + '/paramerrors/callable_object?param2=bar', + 'param1=foo', + error_msgs[3], + ), ): for show_mismatched_params in (True, False): cherrypy.config.update( - {'request.show_mismatched_params': show_mismatched_params}) + {'request.show_mismatched_params': show_mismatched_params}, + ) self.getPage(uri, method='POST', body=body) self.assertStatus(404) if show_mismatched_params: @@ -539,10 +625,10 @@ def testParamErrors(self): # In the case that a handler raises a TypeError we should # let that type error through. for uri in ( - '/paramerrors/raise_type_error', - '/paramerrors/raise_type_error_with_default_param?x=0', - '/paramerrors/raise_type_error_with_default_param?x=0&y=0', - '/paramerrors/raise_type_error_decorated', + '/paramerrors/raise_type_error', + '/paramerrors/raise_type_error_with_default_param?x=0', + '/paramerrors/raise_type_error_with_default_param?x=0&y=0', + '/paramerrors/raise_type_error_decorated', ): self.getPage(uri, method='GET') self.assertStatus(500) @@ -563,8 +649,11 @@ def testErrorHandling(self): self.getPage('/error/page_yield') self.assertErrorPage(500, pattern=valerr) - if (cherrypy.server.protocol_version == 'HTTP/1.0' or - getattr(cherrypy.server, 'using_apache', False)): + if cherrypy.server.protocol_version == 'HTTP/1.0' or getattr( + cherrypy.server, + 'using_apache', + False, + ): self.getPage('/error/page_streamed') # Because this error is raised after the response body has # started, the status should not change to an error status. @@ -573,8 +662,11 @@ def testErrorHandling(self): else: # Under HTTP/1.1, the chunked transfer-coding is used. # The HTTP client will choke when the output is incomplete. - self.assertRaises((ValueError, IncompleteRead), self.getPage, - '/error/page_streamed') + self.assertRaises( + (ValueError, IncompleteRead), + self.getPage, + '/error/page_streamed', + ) # No traceback should be present self.getPage('/error/cause_err_in_finalize') @@ -597,14 +689,16 @@ def testErrorHandling(self): self.assertStatus(401) self.assertBody( 'Error 401 Unauthorized - ' - "Well, I'm very sorry but you haven't paid!") + "Well, I'm very sorry but you haven't paid!", + ) # Test default custom error page. self.getPage('/error/custom_default') self.assertStatus(500) self.assertBody( 'Error 500 Internal Server Error - ' - "Well, I'm very sorry but you haven't paid!".ljust(513)) + "Well, I'm very sorry but you haven't paid!".ljust(513), + ) # Test error in custom error page (ticket #305). # Note that the message is escaped for HTML (ticket #310). @@ -614,10 +708,12 @@ def testErrorHandling(self): exc_name = 'FileNotFoundError' else: exc_name = 'IOError' - msg = ('No, <b>really</b>, not found!
' - 'In addition, the custom error page failed:\n
' - '%s: [Errno 2] ' - "No such file or directory: 'nonexistent.html'") % (exc_name,) + msg = ( + 'No, <b>really</b>, not found!
' + 'In addition, the custom error page failed:\n
' + '%s: [Errno 2] ' + "No such file or directory: 'nonexistent.html'" + ) % (exc_name,) self.assertInBody(msg) if getattr(cherrypy.server, 'using_apache', False): @@ -640,98 +736,114 @@ def testHeaderElements(self): h = [('Accept', 'audio/*; q=0.2, audio/basic')] self.getPage('/headerelements/get_elements?headername=Accept', h) self.assertStatus(200) - self.assertBody('audio/basic\n' - 'audio/*;q=0.2') + self.assertBody('audio/basic\naudio/*;q=0.2') h = [ - ('Accept', - 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c') + ( + 'Accept', + 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c', + ), ] self.getPage('/headerelements/get_elements?headername=Accept', h) self.assertStatus(200) - self.assertBody('text/x-c\n' - 'text/html\n' - 'text/x-dvi;q=0.8\n' - 'text/plain;q=0.5') + self.assertBody( + 'text/x-c\ntext/html\ntext/x-dvi;q=0.8\ntext/plain;q=0.5', + ) # Test that more specific media ranges get priority. h = [('Accept', 'text/*, text/html, text/html;level=1, */*')] self.getPage('/headerelements/get_elements?headername=Accept', h) self.assertStatus(200) - self.assertBody('text/html;level=1\n' - 'text/html\n' - 'text/*\n' - '*/*') + self.assertBody('text/html;level=1\ntext/html\ntext/*\n*/*') # Test Accept-Charset h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')] self.getPage( - '/headerelements/get_elements?headername=Accept-Charset', h) + '/headerelements/get_elements?headername=Accept-Charset', + h, + ) self.assertStatus('200 OK') - self.assertBody('iso-8859-5\n' - 'unicode-1-1;q=0.8') + self.assertBody('iso-8859-5\nunicode-1-1;q=0.8') # Test Accept-Encoding h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')] self.getPage( - '/headerelements/get_elements?headername=Accept-Encoding', h) + '/headerelements/get_elements?headername=Accept-Encoding', + h, + ) self.assertStatus('200 OK') - self.assertBody('gzip;q=1.0\n' - 'identity;q=0.5\n' - '*;q=0') + self.assertBody('gzip;q=1.0\nidentity;q=0.5\n*;q=0') # Test Accept-Language h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')] self.getPage( - '/headerelements/get_elements?headername=Accept-Language', h) + '/headerelements/get_elements?headername=Accept-Language', + h, + ) self.assertStatus('200 OK') - self.assertBody('da\n' - 'en-gb;q=0.8\n' - 'en;q=0.7') + self.assertBody('da\nen-gb;q=0.8\nen;q=0.7') # Test malformed header parsing. See # https://github.com/cherrypy/cherrypy/issues/763. - self.getPage('/headerelements/get_elements?headername=Content-Type', - # Note the illegal trailing ";" - headers=[('Content-Type', 'text/html; charset=utf-8;')]) + self.getPage( + '/headerelements/get_elements?headername=Content-Type', + # Note the illegal trailing ";" + headers=[('Content-Type', 'text/html; charset=utf-8;')], + ) self.assertStatus(200) self.assertBody('text/html;charset=utf-8') def test_repeated_headers(self): # Test that two request headers are collapsed into one. # See https://github.com/cherrypy/cherrypy/issues/542. - self.getPage('/headers/Accept-Charset', - headers=[('Accept-Charset', 'iso-8859-5'), - ('Accept-Charset', 'unicode-1-1;q=0.8')]) + self.getPage( + '/headers/Accept-Charset', + headers=[ + ('Accept-Charset', 'iso-8859-5'), + ('Accept-Charset', 'unicode-1-1;q=0.8'), + ], + ) self.assertBody('iso-8859-5, unicode-1-1;q=0.8') # Tests that each header only appears once, regardless of case. self.getPage('/headers/doubledheaders') self.assertBody('double header test') hnames = [name.title() for name, val in self.headers] - for key in ['Content-Length', 'Content-Type', 'Date', - 'Expires', 'Location', 'Server']: + for key in [ + 'Content-Length', + 'Content-Type', + 'Date', + 'Expires', + 'Location', + 'Server', + ]: self.assertEqual(hnames.count(key), 1, self.headers) def test_encoded_headers(self): # First, make sure the innards work like expected. self.assertEqual( - httputil.decode_TEXT(ntou('=?utf-8?q?f=C3=BCr?=')), ntou('f\xfcr')) + httputil.decode_TEXT(ntou('=?utf-8?q?f=C3=BCr?=')), + ntou('f\xfcr'), + ) if cherrypy.server.protocol_version == 'HTTP/1.1': # Test RFC-2047-encoded request and response header values u = ntou('\u212bngstr\xf6m', 'escape') c = ntou('=E2=84=ABngstr=C3=B6m') - self.getPage('/headers/ifmatch', - [('If-Match', ntou('=?utf-8?q?%s?=') % c)]) + self.getPage( + '/headers/ifmatch', + [('If-Match', ntou('=?utf-8?q?%s?=') % c)], + ) # The body should be utf-8 encoded. self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m') # But the Etag header should be RFC-2047 encoded (binary) self.assertHeader('ETag', ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?=')) # Test a *LONG* RFC-2047-encoded request and response header value - self.getPage('/headers/ifmatch', - [('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))]) + self.getPage( + '/headers/ifmatch', + [('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))], + ) self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m' * 10) # Note: this is different output for Python3, but it decodes fine. etag = self.assertHeader( @@ -739,20 +851,22 @@ def test_encoded_headers(self): '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' - '4oSrbmdzdHLDtm0=?=') + '4oSrbmdzdHLDtm0=?=', + ) self.assertEqual(httputil.decode_TEXT(etag), u * 10) def test_header_presence(self): # If we don't pass a Content-Type header, it should not be present # in cherrypy.request.headers - self.getPage('/headers/Content-Type', - headers=[]) + self.getPage('/headers/Content-Type', headers=[]) self.assertStatus(500) # If Content-Type is present in the request, it should be present in # cherrypy.request.headers - self.getPage('/headers/Content-Type', - headers=[('Content-type', 'application/json')]) + self.getPage( + '/headers/Content-Type', + headers=[('Content-type', 'application/json')], + ) self.assertBody('application/json') def test_dangerous_host(self): @@ -766,8 +880,12 @@ def test_dangerous_host(self): self.assertBody('foobar') def test_basic_HTTPMethods(self): - helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND', - 'PATCH') + helper.webtest.methods_with_bodies = ( + 'POST', + 'PUT', + 'PROPFIND', + 'PATCH', + ) # Test that all defined HTTP methods work. for m in defined_http_methods: @@ -784,14 +902,16 @@ def test_basic_HTTPMethods(self): # test of PATCH requests # Request a PATCH method with a form-urlencoded body - self.getPage('/method/parameterized', method='PATCH', - body='data=on+top+of+other+things') + self.getPage( + '/method/parameterized', + method='PATCH', + body='data=on+top+of+other+things', + ) self.assertBody('on top of other things') # Request a PATCH method with a file body b = 'one thing on top of another' - h = [('Content-Type', 'text/plain'), - ('Content-Length', str(len(b)))] + h = [('Content-Type', 'text/plain'), ('Content-Length', str(len(b)))] self.getPage('/method/request_body', headers=h, method='PATCH', body=b) self.assertStatus(200) self.assertBody(b) @@ -824,14 +944,16 @@ def test_basic_HTTPMethods(self): # HTTP PUT tests # Request a PUT method with a form-urlencoded body - self.getPage('/method/parameterized', method='PUT', - body='data=on+top+of+other+things') + self.getPage( + '/method/parameterized', + method='PUT', + body='data=on+top+of+other+things', + ) self.assertBody('on top of other things') # Request a PUT method with a file body b = 'one thing on top of another' - h = [('Content-Type', 'text/plain'), - ('Content-Length', str(len(b)))] + h = [('Content-Type', 'text/plain'), ('Content-Length', str(len(b)))] self.getPage('/method/request_body', headers=h, method='PUT', body=b) self.assertStatus(200) self.assertBody(b) @@ -863,13 +985,18 @@ def test_basic_HTTPMethods(self): self.assertStatus(411) # Request a custom method with a request body - b = ('\n\n' - '' - '') - h = [('Content-Type', 'text/xml'), - ('Content-Length', str(len(b)))] - self.getPage('/method/request_body', headers=h, - method='PROPFIND', body=b) + b = ( + '\n\n' + '' + '' + ) + h = [('Content-Type', 'text/xml'), ('Content-Length', str(len(b)))] + self.getPage( + '/method/request_body', + headers=h, + method='PROPFIND', + body=b, + ) self.assertStatus(200) self.assertBody(b) @@ -916,8 +1043,14 @@ def test_CONNECT_method(self): self.persistent = False def test_CONNECT_method_invalid_authority(self): - for request_target in ['example.com', 'http://example.com:33', - '/path/', 'path/', '/?q=f', '#f']: + for request_target in [ + 'example.com', + 'http://example.com:33', + '/path/', + 'path/', + '/?q=f', + '#f', + ]: self.persistent = True try: conn = self.HTTP_CONN @@ -926,8 +1059,10 @@ def test_CONNECT_method_invalid_authority(self): response.begin() self.assertEqual(response.status, 400) self.body = response.read() - self.assertBody(b'Invalid path in Request-URI: request-target ' - b'must match authority-form.') + self.assertBody( + b'Invalid path in Request-URI: request-target ' + b'must match authority-form.', + ) finally: self.persistent = False diff --git a/cherrypy/test/test_routes.py b/cherrypy/test/test_routes.py index cc7147654..72dc49a5c 100644 --- a/cherrypy/test/test_routes.py +++ b/cherrypy/test/test_routes.py @@ -1,4 +1,5 @@ """Test Routes dispatcher.""" + import os import importlib @@ -22,22 +23,22 @@ def setup_server(): pytest.skip('Install routes to test RoutesDispatcher code') class Dummy: - def index(self): return 'I said good day!' class City: - def __init__(self, name): self.name = name self.population = 10000 - @cherrypy.config(**{ - 'tools.response_headers.on': True, - 'tools.response_headers.headers': [ - ('Content-Language', 'en-GB'), - ], - }) + @cherrypy.config( + **{ + 'tools.response_headers.on': True, + 'tools.response_headers.headers': [ + ('Content-Language', 'en-GB'), + ], + }, + ) def index(self, **kwargs): return 'Welcome to %s, pop. %s' % (self.name, self.population) @@ -46,13 +47,25 @@ def update(self, **kwargs): return 'OK' d = cherrypy.dispatch.RoutesDispatcher() - d.connect(action='index', name='hounslow', route='/hounslow', - controller=City('Hounslow')) d.connect( - name='surbiton', route='/surbiton', controller=City('Surbiton'), - action='index', conditions=dict(method=['GET'])) - d.mapper.connect('/surbiton', controller='surbiton', - action='update', conditions=dict(method=['POST'])) + action='index', + name='hounslow', + route='/hounslow', + controller=City('Hounslow'), + ) + d.connect( + name='surbiton', + route='/surbiton', + controller=City('Surbiton'), + action='index', + conditions=dict(method=['GET']), + ) + d.mapper.connect( + '/surbiton', + controller='surbiton', + action='update', + conditions=dict(method=['POST']), + ) d.connect('main', ':action', controller=Dummy()) conf = {'/': {'request.dispatch': d}} diff --git a/cherrypy/test/test_session.py b/cherrypy/test/test_session.py index 162454b23..0cce9b4ea 100755 --- a/cherrypy/test/test_session.py +++ b/cherrypy/test/test_session.py @@ -32,16 +32,16 @@ def http_methods_allowed(methods=['GET', 'HEAD']): def setup_server(): - - @cherrypy.config(**{ - 'tools.sessions.on': True, - 'tools.sessions.storage_class': sessions.RamSession, - 'tools.sessions.storage_path': localDir, - 'tools.sessions.timeout': (1.0 / 60), - 'tools.sessions.clean_freq': (1.0 / 60), - }) + @cherrypy.config( + **{ + 'tools.sessions.on': True, + 'tools.sessions.storage_class': sessions.RamSession, + 'tools.sessions.storage_path': localDir, + 'tools.sessions.timeout': (1.0 / 60), + 'tools.sessions.clean_freq': (1.0 / 60), + }, + ) class Root: - @cherrypy.expose def clear(self): cherrypy.session.cache.clear() @@ -109,10 +109,12 @@ def iredir(self): raise cherrypy.InternalRedirect('/redir_target') @cherrypy.expose - @cherrypy.config(**{ - 'tools.allow.on': True, - 'tools.allow.methods': ['GET'], - }) + @cherrypy.config( + **{ + 'tools.allow.on': True, + 'tools.allow.methods': ['GET'], + }, + ) def restricted(self): return cherrypy.request.method @@ -126,11 +128,13 @@ def length(self): return str(len(cherrypy.session)) @cherrypy.expose - @cherrypy.config(**{ - 'tools.sessions.path': '/session_cookie', - 'tools.sessions.name': 'temp', - 'tools.sessions.persistent': False, - }) + @cherrypy.config( + **{ + 'tools.sessions.path': '/session_cookie', + 'tools.sessions.name': 'temp', + 'tools.sessions.persistent': False, + }, + ) def session_cookie(self): # Must load() to start the clean thread. cherrypy.session.load() @@ -151,9 +155,7 @@ def teardown_class(cls): consume( file.remove_p() for file in files_to_clean - if file.basename().startswith( - sessions.FileSession.SESSION_PREFIX - ) + if file.basename().startswith(sessions.FileSession.SESSION_PREFIX) ) # FIXME: This test is unstable on slow envs like macOS @@ -172,8 +174,9 @@ def test_0_Session(self): self.getPage('/testStr') assert self.body == b'1' - cookie_parts = dict([p.strip().split('=') - for p in self.cookies[0][1].split(';')]) + cookie_parts = dict( + [p.strip().split('=') for p in self.cookies[0][1].split(';')], + ) # Assert there is an 'expires' param expected_cookie_keys = {'session_id', 'expires', 'Path', 'Max-Age'} assert set(cookie_parts.keys()) == expected_cookie_keys @@ -228,6 +231,7 @@ def f(): for x in os.listdir(localDir) if x.startswith('session-') and not x.endswith('.lock') ] + assert f() == [] # Wait for the cleanup thread to delete remaining session files @@ -337,11 +341,16 @@ def test_6_regenerate(self): self.getPage('/testStr') # grab the cookie ID id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] - self.getPage('/testStr', - headers=[ - ('Cookie', - 'session_id=maliciousid; ' - 'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')]) + self.getPage( + '/testStr', + headers=[ + ( + 'Cookie', + 'session_id=maliciousid; ' + 'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;', + ), + ], + ) id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1] assert id1 != id2 assert id2 != 'maliciousid' @@ -351,8 +360,9 @@ def test_7_session_cookies(self): self.getPage('/clear') self.getPage('/session_cookie') # grab the cookie ID - cookie_parts = dict([p.strip().split('=') - for p in self.cookies[0][1].split(';')]) + cookie_parts = dict( + [p.strip().split('=') for p in self.cookies[0][1].split(';')], + ) # Assert there is no 'expires' param assert set(cookie_parts.keys()) == {'temp', 'Path'} id1 = cookie_parts['temp'] @@ -360,8 +370,9 @@ def test_7_session_cookies(self): # 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(';')]) + cookie_parts = dict( + [p.strip().split('=') for p in self.cookies[0][1].split(';')], + ) # Assert there is no 'expires' param assert set(cookie_parts.keys()) == {'temp', 'Path'} assert self.body.decode('utf-8') == id1 @@ -370,8 +381,9 @@ def test_7_session_cookies(self): # Simulate a browser close by just not sending the cookies self.getPage('/session_cookie') # grab the cookie ID - cookie_parts = dict([p.strip().split('=') - for p in self.cookies[0][1].split(';')]) + cookie_parts = dict( + [p.strip().split('=') for p in self.cookies[0][1].split(';')], + ) # Assert there is no 'expires' param assert set(cookie_parts.keys()) == {'temp', 'Path'} # Assert a new id has been generated... @@ -447,7 +459,8 @@ def is_occupied(): @pytest.fixture def memcached_configured( - memcached_instance, monkeypatch, + memcached_instance, + monkeypatch, memcached_client_present, ): server = 'localhost:{port}'.format_map(memcached_instance) @@ -467,9 +480,7 @@ 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('/set_session_cls/cherrypy.lib.sessions.MemcachedSession') self.getPage('/testStr') assert self.body == b'1' @@ -540,8 +551,7 @@ def test_3_Redirect(self): def test_5_Error_paths(self): self.getPage('/unknown/page') - self.assertErrorPage( - 404, "The path '/unknown/page' was not found.") + 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 diff --git a/cherrypy/test/test_sessionauthenticate.py b/cherrypy/test/test_sessionauthenticate.py index 63053fcb7..480ed39ad 100644 --- a/cherrypy/test/test_sessionauthenticate.py +++ b/cherrypy/test/test_sessionauthenticate.py @@ -3,10 +3,8 @@ class SessionAuthenticateTest(helper.CPWebCase): - @staticmethod def setup_server(): - def check(username, password): # Dummy check_username_and_password function if username != 'test' or password != 'password': @@ -19,10 +17,13 @@ def augment_params(): cherrypy.request.params['test'] = 'test' cherrypy.tools.augment_params = cherrypy.Tool( - 'before_handler', augment_params, None, priority=30) + 'before_handler', + augment_params, + None, + priority=30, + ) class Test: - _cp_config = { 'tools.sessions.on': True, 'tools.session_auth.on': True, diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py index 64177762a..b04701cd6 100644 --- a/cherrypy/test/test_states.py +++ b/cherrypy/test/test_states.py @@ -16,7 +16,6 @@ class Dependency: - def __init__(self, bus): self.bus = bus self.running = False @@ -53,7 +52,6 @@ def stopthread(self, thread_id): def setup_server(): class Root: - @cherrypy.expose def index(self): return 'Hello World' @@ -68,12 +66,15 @@ def graceful(self): return 'app was (gracefully) restarted succesfully' cherrypy.tree.mount(Root()) - cherrypy.config.update({ - 'environment': 'test_suite', - }) + cherrypy.config.update( + { + 'environment': 'test_suite', + }, + ) db_connection.subscribe() + # ------------ Enough helpers. Time for real live test cases. ------------ # @@ -121,6 +122,7 @@ def exittest(): self.getPage('/') self.assertBody('Hello World') engine.exit() + cherrypy.server.start() engine.start_with_callback(exittest) engine.block() @@ -254,8 +256,10 @@ def test_4_Autoreload(self): self.getPage('/start') if not (float(self.body) > start): - raise AssertionError('start time %s not greater than %s' % - (float(self.body), start)) + raise AssertionError( + 'start time %s not greater than %s' + % (float(self.body), start), + ) finally: # Shut down the spawned process self.getPage('/exit') @@ -269,12 +273,11 @@ def test_5_Start_Error(self): # If a process errors during start, it should stop the engine # and exit with a non-zero exit code. - p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'), - wait=True) + p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'), wait=True) p.write_conf( extra="""starterror: True test_case_name: "test_5_Start_Error" -""" +""", ) p.start(imports='cherrypy.test._test_states_demo') if p.exit_code == 0: @@ -282,7 +285,6 @@ def test_5_Start_Error(self): class PluginTests(helper.CPWebCase): - def test_daemonize(self): if os.name not in ['posix']: return self.skip('skipped (not on posix) ') @@ -291,12 +293,14 @@ def test_daemonize(self): # Spawn the process and wait, when this returns, the original process # is finished. If it daemonized properly, we should still be able # to access pages. - p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'), - wait=True, daemonize=True, - socket_host='127.0.0.1', - socket_port=8081) - p.write_conf( - extra='test_case_name: "test_daemonize"') + p = helper.CPProcess( + ssl=(self.scheme.lower() == 'https'), + wait=True, + daemonize=True, + socket_host='127.0.0.1', + socket_port=8081, + ) + p.write_conf(extra='test_case_name: "test_daemonize"') p.start(imports='cherrypy.test._test_states_demo') try: # Just get the pid of the daemonization process. @@ -316,7 +320,6 @@ def test_daemonize(self): class SignalHandlingTests(helper.CPWebCase): - def test_SIGHUP_tty(self): # When not daemonized, SIGHUP should shut down the server. try: @@ -326,8 +329,7 @@ def test_SIGHUP_tty(self): # Spawn the process. p = helper.CPProcess(ssl=(self.scheme.lower() == 'https')) - p.write_conf( - extra='test_case_name: "test_SIGHUP_tty"') + p.write_conf(extra='test_case_name: "test_SIGHUP_tty"') p.start(imports='cherrypy.test._test_states_demo') # Send a SIGHUP os.kill(p.get_pid(), SIGHUP) @@ -347,10 +349,12 @@ def test_SIGHUP_daemonized(self): # Spawn the process and wait, when this returns, the original process # is finished. If it daemonized properly, we should still be able # to access pages. - p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'), - wait=True, daemonize=True) - p.write_conf( - extra='test_case_name: "test_SIGHUP_daemonized"') + p = helper.CPProcess( + ssl=(self.scheme.lower() == 'https'), + wait=True, + daemonize=True, + ) + p.write_conf(extra='test_case_name: "test_SIGHUP_daemonized"') p.start(imports='cherrypy.test._test_states_demo') pid = p.get_pid() @@ -376,13 +380,12 @@ def _require_signal_and_kill(self, signal_name): self.skip('skipped (no os.kill)') def test_SIGTERM(self): - 'SIGTERM should shut down the server whether daemonized or not.' + "SIGTERM should shut down the server whether daemonized or not." self._require_signal_and_kill('SIGTERM') # Spawn a normal, undaemonized process. p = helper.CPProcess(ssl=(self.scheme.lower() == 'https')) - p.write_conf( - extra='test_case_name: "test_SIGTERM"') + p.write_conf(extra='test_case_name: "test_SIGTERM"') p.start(imports='cherrypy.test._test_states_demo') # Send a SIGTERM os.kill(p.get_pid(), signal.SIGTERM) @@ -391,10 +394,12 @@ def test_SIGTERM(self): if os.name in ['posix']: # Spawn a daemonized process and test again. - p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'), - wait=True, daemonize=True) - p.write_conf( - extra='test_case_name: "test_SIGTERM_2"') + p = helper.CPProcess( + ssl=(self.scheme.lower() == 'https'), + wait=True, + daemonize=True, + ) + p.write_conf(extra='test_case_name: "test_SIGTERM_2"') p.start(imports='cherrypy.test._test_states_demo') # Send a SIGTERM os.kill(p.get_pid(), signal.SIGTERM) @@ -416,7 +421,8 @@ def test_signal_handler_unsubscribe(self): p.write_conf( extra="""unsubsig: True test_case_name: "test_signal_handler_unsubscribe" -""") +""", + ) p.start(imports='cherrypy.test._test_states_demo') # Ask the process to quit os.kill(p.get_pid(), signal.SIGTERM) @@ -455,8 +461,8 @@ def test_safe_wait_INADDR_ANY(): # pylint: disable=invalid-name # Wait on the free port that's unbound with pytest.warns( - UserWarning, - match='Unable to verify that the server is bound on ', + UserWarning, + match='Unable to verify that the server is bound on ', ) as warnings: # pylint: disable=protected-access with servers._safe_wait(inaddr_any, free_port): diff --git a/cherrypy/test/test_static.py b/cherrypy/test/test_static.py index bfe2f40fe..a6008adfa 100644 --- a/cherrypy/test/test_static.py +++ b/cherrypy/test/test_static.py @@ -53,7 +53,7 @@ def ensure_unicode_filesystem(): # 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 # test_file_stream. -MB = 2 ** 20 +MB = 2**20 BIGFILE_SIZE = 32 * MB @@ -66,15 +66,14 @@ def setup_server(): with open(has_space_filepath, 'wb') as f: f.write(b'Hello, world\r\n') needs_bigfile = ( - not os.path.exists(bigfile_filepath) or - os.path.getsize(bigfile_filepath) != BIGFILE_SIZE + not os.path.exists(bigfile_filepath) + or os.path.getsize(bigfile_filepath) != BIGFILE_SIZE ) if needs_bigfile: with open(bigfile_filepath, 'wb') as f: f.write(b'x' * BIGFILE_SIZE) class Root: - @cherrypy.expose @cherrypy.config(**{'response.stream': True}) def bigfile(self): @@ -102,17 +101,18 @@ def serve_file_utf8_filename(self): return static.serve_file( __file__, disposition='attachment', - name='has_utf-8_character_☃.html') + name='has_utf-8_character_☃.html', + ) @cherrypy.expose def serve_fileobj_utf8_filename(self): return static.serve_fileobj( io.BytesIO('☃\nfie\nfo\nfum'.encode('utf-8')), disposition='attachment', - name='has_utf-8_character_☃.html') + name='has_utf-8_character_☃.html', + ) class Static: - @cherrypy.expose def index(self): return 'You want the Baron? You can have the Baron!' @@ -153,7 +153,7 @@ def dynamic(self): 'tools.staticdir.root': curdir, 'tools.staticdir.dir': 'static', 'error_page.404': error_page_404, - } + }, } rootApp = cherrypy.Application(root) rootApp.merge(rootconf) @@ -211,8 +211,10 @@ def test_static(self): ascii_fn = 'has_utf-8_character_.html' url_quote_fn = 'has_utf-8_character_%E2%98%83.html' # %E2%98%83 == ☃ expected_content_disposition = ( - 'attachment; filename="{!s}"; filename*=UTF-8\'\'{!s}'. - format(ascii_fn, url_quote_fn) + 'attachment; filename="{!s}"; filename*=UTF-8\'\'{!s}'.format( + ascii_fn, + url_quote_fn, + ) ) self.getPage('/serve_file_utf8_filename') @@ -254,9 +256,8 @@ def test_index(self): self.assertStatus(301) self.assertHeader('Location', '%s/docroot/' % self.base()) self.assertMatchesBody( - "This resource .* " - '%s/docroot/.' - % (self.base(), self.base()) + 'This resource .* ' + '%s/docroot/.' % (self.base(), self.base()), ) def test_config_errors(self): @@ -347,8 +348,12 @@ def test_file_stream(self): else: newconn = HTTPConnection s, h, b = helper.webtest.openURL( - b'/tell', headers=[], host=self.HOST, port=self.PORT, - http_conn=newconn) + b'/tell', + headers=[], + host=self.HOST, + port=self.PORT, + http_conn=newconn, + ) if not b: # The file was closed on the server. tell_position = BIGFILE_SIZE @@ -382,17 +387,21 @@ def test_file_stream(self): 'The file should have advanced to position %r, but ' 'has already advanced to the end of the file. It ' 'may not be streamed as intended, or at the wrong ' - 'chunk size (64k)' % read_so_far) + 'chunk size (64k)' % read_so_far, + ) elif tell_position < read_so_far: self.fail( 'The file should have advanced to position %r, but has ' 'only advanced to position %r. It may not be streamed ' - 'as intended, or at the wrong chunk size (64k)' % - (read_so_far, tell_position)) + 'as intended, or at the wrong chunk size (64k)' + % (read_so_far, tell_position), + ) if body != b'x' * BIGFILE_SIZE: - self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % - (BIGFILE_SIZE, body[:50], len(body))) + self.fail( + "Body != 'x' * %d. Got %r instead (%d bytes)." + % (BIGFILE_SIZE, body[:50], len(body)), + ) conn.close() def test_file_stream_deadlock(self): @@ -412,8 +421,10 @@ def test_file_stream_deadlock(self): self.assertEqual(response.status, 200) body = response.fp.read(65536) if body != b'x' * len(body): - self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % - (65536, body[:50], len(body))) + self.fail( + "Body != 'x' * %d. Got %r instead (%d bytes)." + % (65536, body[:50], len(body)), + ) response.close() conn.close() @@ -421,8 +432,10 @@ def test_file_stream_deadlock(self): self.persistent = False self.getPage('/bigfile') if self.body != b'x' * BIGFILE_SIZE: - self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % - (BIGFILE_SIZE, self.body[:50], len(body))) + self.fail( + "Body != 'x' * %d. Got %r instead (%d bytes)." + % (BIGFILE_SIZE, self.body[:50], len(body)), + ) def test_error_page_with_serve_file(self): self.getPage('/404test/yunyeen') @@ -442,7 +455,7 @@ def test_null_bytes(self): def unicode_file(cls): filename = ntou('Слава Україні.html', 'utf-8') filepath = curdir / 'static' / filename - with filepath.open('w', encoding='utf-8')as strm: + with filepath.open('w', encoding='utf-8') as strm: strm.write(ntou('Героям Слава!', 'utf-8')) cls.files_to_remove.append(filepath) diff --git a/cherrypy/test/test_tools.py b/cherrypy/test/test_tools.py index 7ef60917a..9d864feeb 100644 --- a/cherrypy/test/test_tools.py +++ b/cherrypy/test/test_tools.py @@ -22,18 +22,19 @@ class ToolTests(helper.CPWebCase): - @staticmethod def setup_server(): - # Put check_access in a custom toolbox with its own namespace myauthtools = cherrypy._cptools.Toolbox('myauth') def check_access(default=False): if not getattr(cherrypy.request, 'userid', default): raise cherrypy.HTTPError(401) + myauthtools.check_access = cherrypy.Tool( - 'before_request_body', check_access) + 'before_request_body', + check_access, + ) def numerify(): def number_it(body): @@ -41,19 +42,22 @@ def number_it(body): for k, v in cherrypy.request.numerify_map: chunk = chunk.replace(k, v) yield chunk + cherrypy.response.body = number_it(cherrypy.response.body) class NumTool(cherrypy.Tool): - def _setup(self): def makemap(): m = self._merged_args().get('map', {}) cherrypy.request.numerify_map = list(m.items()) + cherrypy.request.hooks.attach('on_start_resource', makemap) def critical(): cherrypy.request.error_response = cherrypy.HTTPError( - 502).set_response + 502, + ).set_response + critical.failsafe = True cherrypy.request.hooks.attach('on_start_resource', critical) @@ -63,7 +67,6 @@ def critical(): # It's not mandatory to inherit from cherrypy.Tool. class NadsatTool: - def __init__(self): self.ended = {} self._name = 'nadsat' @@ -74,7 +77,9 @@ def nadsat_it_up(body): chunk = chunk.replace(b'good', b'horrorshow') chunk = chunk.replace(b'piece', b'lomtick') yield chunk + cherrypy.response.body = nadsat_it_up(cherrypy.response.body) + nadsat.priority = 0 def cleanup(self): @@ -83,11 +88,13 @@ def cleanup(self): id = cherrypy.request.params.get('id') if id: self.ended[id] = True + cleanup.failsafe = True def _setup(self): cherrypy.request.hooks.attach('before_finalize', self.nadsat) cherrypy.request.hooks.attach('on_end_request', self.cleanup) + tools.nadsat = NadsatTool() def pipe_body(): @@ -97,11 +104,11 @@ def pipe_body(): # Assert that we can use a callable object instead of a function. class Rotator(object): - def __call__(self, scale): r = cherrypy.response r.collapse_body() 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): @@ -115,20 +122,23 @@ def stream_handler(next_handler, *args, **kwargs): return o.getvalue() finally: o.close() + cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool( - stream_handler) + stream_handler, + ) class Root: - @cherrypy.expose def index(self): return 'Howdy earth!' @cherrypy.expose - @cherrypy.config(**{ - 'tools.streamer.on': True, - 'tools.streamer.arg': 'arg value', - }) + @cherrypy.config( + **{ + 'tools.streamer.on': True, + 'tools.streamer.arg': 'arg value', + }, + ) def tarfile(self): actual = cherrypy.request.config.get('tools.streamer.arg') assert actual == 'arg value' @@ -160,6 +170,7 @@ def decorated_euro(self, *vpath): yield ntou('Hello,') yield ntou('world') yield europoundUnicode + decorated_euro = tools.gzip(compress_level=6)(decorated_euro) decorated_euro = tools.rotator(scale=3)(decorated_euro) @@ -170,19 +181,20 @@ class TestType(type): subclass, and adds an instance of the subclass as an attribute of root. """ + def __init__(cls, name, bases, dct): type.__init__(cls, name, bases, dct) for value in dct.values(): if isinstance(value, types.FunctionType): cherrypy.expose(value) setattr(root, name.lower(), cls()) + Test = TestType('Test', (object,), {}) # METHOD ONE: # Declare Tools in _cp_config @cherrypy.config(**{'tools.nadsat.on': True}) class Demo(Test): - def index(self, id=None): return 'A good piece of cherry pie' @@ -203,6 +215,7 @@ def errinstream(self, id=None): # @tools.check_access() def restricted(self): return 'Welcome!' + restricted = myauthtools.check_access()(restricted) userid = restricted @@ -233,7 +246,7 @@ def stream(self, id=None): }, '/demo/err_in_onstart': { # Because this isn't a dict, on_start_resource will error. - 'tools.numerify.map': 'pie->3.14159' + 'tools.numerify.map': 'pie->3.14159', }, # Combined tools '/euro': { @@ -245,7 +258,7 @@ def stream(self, id=None): 'tools.gzip.priority': 10, }, # Handler wrappers - '/tarfile': {'tools.streamer.on': True} + '/tarfile': {'tools.streamer.on': True}, } app = cherrypy.tree.mount(root, config=conf) app.request_class.namespaces['myauth'] = myauthtools @@ -271,8 +284,11 @@ def testHookErrors(self): self.assertBody('True') # If body is "razdrez", then on_end_request is being called too early. - if (cherrypy.server.protocol_version == 'HTTP/1.0' or - getattr(cherrypy.server, 'using_apache', False)): + if cherrypy.server.protocol_version == 'HTTP/1.0' or getattr( + cherrypy.server, + 'using_apache', + False, + ): self.getPage('/demo/errinstream?id=5') # Because this error is raised after the response body has # started, the status should not change to an error status. @@ -282,8 +298,11 @@ def testHookErrors(self): # Because this error is raised after the response body has # started, and because it's chunked output, an error is raised by # the HTTP client when it encounters incomplete output. - self.assertRaises((ValueError, IncompleteRead), self.getPage, - '/demo/errinstream?id=5') + self.assertRaises( + (ValueError, IncompleteRead), + self.getPage, + '/demo/errinstream?id=5', + ) # If this fails, then on_end_request isn't being called at all. time.sleep(0.1) self.getPage('/demo/ended/5') @@ -343,17 +362,21 @@ def testGuaranteedHooks(self): self.assertInBody(expected_msg) def testCombinedTools(self): - expectedResult = (ntou('Hello,world') + - europoundUnicode).encode('utf-8') + expectedResult = (ntou('Hello,world') + europoundUnicode).encode( + 'utf-8', + ) zbuf = io.BytesIO() zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9) zfile.write(expectedResult) zfile.close() - self.getPage('/euro', - headers=[ - ('Accept-Encoding', 'gzip'), - ('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7')]) + self.getPage( + '/euro', + headers=[ + ('Accept-Encoding', 'gzip'), + ('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'), + ], + ) self.assertInBody(zbuf.getvalue()[:3]) zbuf = io.BytesIO() @@ -368,16 +391,23 @@ def testCombinedTools(self): # lowered in conf, allowing the rotator to run after gzip. # Of course, we don't want breakage in production apps, # but it proves the priority was changed. - self.getPage('/decorated_euro/subpath', - headers=[('Accept-Encoding', 'gzip')]) + self.getPage( + '/decorated_euro/subpath', + headers=[('Accept-Encoding', 'gzip')], + ) self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()])) def testBareHooks(self): content = 'bit of a pain in me gulliver' - self.getPage('/pipe', - headers=[('Content-Length', str(len(content))), - ('Content-Type', 'text/plain')], - method='POST', body=content) + self.getPage( + '/pipe', + headers=[ + ('Content-Length', str(len(content))), + ('Content-Type', 'text/plain'), + ], + method='POST', + body=content, + ) self.assertBody(content) def testHandlerWrapperTool(self): @@ -409,14 +439,18 @@ def testDecorator(self): @cherrypy.tools.register('on_start_resource') def example(): pass + self.assertTrue(isinstance(cherrypy.tools.example, cherrypy.Tool)) self.assertEqual(cherrypy.tools.example._point, 'on_start_resource') @cherrypy.tools.register( # noqa: F811 - 'before_finalize', name='renamed', priority=60, + 'before_finalize', + name='renamed', + priority=60, ) def example(): # noqa: F811 pass + self.assertTrue(isinstance(cherrypy.tools.renamed, cherrypy.Tool)) self.assertEqual(cherrypy.tools.renamed._point, 'before_finalize') self.assertEqual(cherrypy.tools.renamed._name, 'renamed') @@ -424,7 +458,6 @@ def example(): # noqa: F811 class SessionAuthTest(unittest.TestCase): - def test_login_screen_returns_bytes(self): """ login_screen must return bytes even if unicode parameters are passed. @@ -432,8 +465,11 @@ def test_login_screen_returns_bytes(self): username and password were unicode. """ sa = cherrypy.lib.cptools.SessionAuth() - res = sa.login_screen(None, username=str('nobody'), - password=str('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 4594faa12..71a69ced7 100644 --- a/cherrypy/test/test_tutorials.py +++ b/cherrypy/test/test_tutorials.py @@ -6,12 +6,13 @@ class TutorialTest(helper.CPWebCase): - @classmethod def setup_server(cls): """Mount something so the engine starts.""" + class Dummy: pass + cherrypy.tree.mount(Dummy()) @staticmethod @@ -30,7 +31,7 @@ def setup_tutorial(cls, name, root_name, config={}): module = cls.load_module(name) root = getattr(module, root_name) conf = getattr(module, 'tutconf') - class_types = type, + class_types = (type,) if isinstance(root, class_types): root = root() cherrypy.tree.mount(root, config=conf) @@ -69,7 +70,7 @@ def test03GetAndPost(self): def test04ComplexSite(self): self.setup_tutorial('tut04_complex_site', 'root') - msg = ''' + msg = """

Here are some extra useful links:

    @@ -77,13 +78,13 @@ def test04ComplexSite(self):
  • CherryPy
-

[Return to links page]

''' +

[Return to links page]

""" self.getPage('/links/extra/') self.assertBody(msg) def test05DerivedObjects(self): self.setup_tutorial('tut05_derived_objects', 'HomePage') - msg = ''' + msg = """ Another Page @@ -97,7 +98,7 @@ def test05DerivedObjects(self): - ''' + """ # the tutorial has some annoying spaces in otherwise blank lines msg = msg.replace('\n\n', '\n \n') msg = msg.replace('

\n\n', '

\n \n') @@ -107,8 +108,10 @@ def test05DerivedObjects(self): def test06DefaultMethod(self): self.setup_tutorial('tut06_default_method', 'UsersPage') self.getPage('/hendrik') - self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German ' - '(back)') + self.assertBody( + 'Hendrik Mans, CherryPy co-developer & crazy German ' + '(back)', + ) def test07Sessions(self): self.setup_tutorial('tut07_sessions', 'HitCounter') @@ -117,51 +120,64 @@ def test07Sessions(self): self.assertBody( "\n During your current session, you've viewed this" '\n page 1 times! Your life is a patio of fun!' - '\n ') + '\n ', + ) self.getPage('/', self.cookies) self.assertBody( "\n During your current session, you've viewed this" '\n page 2 times! Your life is a patio of fun!' - '\n ') + '\n ', + ) def test08GeneratorsAndYield(self): self.setup_tutorial('tut08_generators_and_yield', 'GeneratorDemo') self.getPage('/') - self.assertBody('

Generators rule!

' - '

List of users:

' - 'Remi
Carlos
Hendrik
Lorenzo Lamas
' - '') + self.assertBody( + '

Generators rule!

' + '

List of users:

' + 'Remi
Carlos
Hendrik
Lorenzo Lamas
' + '', + ) def test09Files(self): self.setup_tutorial('tut09_files', 'FileDemo') # Test upload filesize = 5 - h = [('Content-type', 'multipart/form-data; boundary=x'), - ('Content-Length', str(105 + filesize))] - b = ('--x\n' - 'Content-Disposition: form-data; name="myFile"; ' - 'filename="hello.txt"\r\n' - 'Content-Type: text/plain\r\n' - '\r\n') + h = [ + ('Content-type', 'multipart/form-data; boundary=x'), + ('Content-Length', str(105 + filesize)), + ] + b = ( + '--x\n' + 'Content-Disposition: form-data; name="myFile"; ' + 'filename="hello.txt"\r\n' + 'Content-Type: text/plain\r\n' + '\r\n' + ) b += 'a' * filesize + '\n' + '--x--\n' self.getPage('/upload', h, 'POST', b) - self.assertBody(''' + self.assertBody( + """ myFile length: %d
myFile filename: hello.txt
myFile mime-type: text/plain - ''' % filesize) + """ + % filesize, + ) # Test download self.getPage('/download') self.assertStatus('200 OK') self.assertHeader('Content-Type', 'application/x-download') - self.assertHeader('Content-Disposition', - # Make sure the filename is quoted. - 'attachment; filename="pdf_file.pdf"') + self.assertHeader( + 'Content-Disposition', + # Make sure the filename is quoted. + 'attachment; filename="pdf_file.pdf"', + ) self.assertEqual(len(self.body), 11961) def test10HTTPErrors(self): @@ -170,6 +186,7 @@ def test10HTTPErrors(self): @cherrypy.expose def traceback_setting(): return repr(cherrypy.request.show_tracebacks) + cherrypy.tree.mount(traceback_setting, '/traceback_setting') self.getPage('/') @@ -188,8 +205,10 @@ def traceback_setting(): self.getPage('/error?code=500') self.assertStatus(500) - self.assertInBody('The server encountered an unexpected condition ' - 'which prevented it from fulfilling the request.') + self.assertInBody( + 'The server encountered an unexpected condition ' + 'which prevented it from fulfilling the request.', + ) self.getPage('/error?code=403') self.assertStatus(403) diff --git a/cherrypy/test/test_virtualhost.py b/cherrypy/test/test_virtualhost.py index de88f9272..4e974979b 100644 --- a/cherrypy/test/test_virtualhost.py +++ b/cherrypy/test/test_virtualhost.py @@ -7,11 +7,9 @@ class VirtualHostTest(helper.CPWebCase): - @staticmethod def setup_server(): class Root: - @cherrypy.expose def index(self): return 'Hello, world' @@ -25,7 +23,6 @@ def method(self, value): return 'You sent %s' % value class VHost: - def __init__(self, sitename): self.sitename = sitename @@ -43,27 +40,35 @@ def url(self): # Test static as a handler (section must NOT include vhost prefix) static = cherrypy.tools.staticdir.handler( - section='/static', dir=curdir) + section='/static', + dir=curdir, + ) root = Root() root.mydom2 = VHost('Domain 2') root.mydom3 = VHost('Domain 3') - hostmap = {'www.mydom2.com': '/mydom2', - 'www.mydom3.com': '/mydom3', - 'www.mydom4.com': '/dom4', - } - cherrypy.tree.mount(root, config={ - '/': { - 'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap) - }, - # Test static in config (section must include vhost prefix) - '/mydom2/static2': { - 'tools.staticdir.on': True, - 'tools.staticdir.root': curdir, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.index': 'index.html', + hostmap = { + 'www.mydom2.com': '/mydom2', + 'www.mydom3.com': '/mydom3', + 'www.mydom4.com': '/dom4', + } + cherrypy.tree.mount( + root, + config={ + '/': { + 'request.dispatch': cherrypy.dispatch.VirtualHost( + **hostmap, + ), + }, + # Test static in config (section must include vhost prefix) + '/mydom2/static2': { + 'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.index': 'index.html', + }, }, - }) + ) def testVirtualHost(self): self.getPage('/', [('Host', 'www.mydom1.com')]) @@ -83,8 +88,12 @@ def testVirtualHost(self): self.assertBody('You sent root') self.getPage('/vmethod?value=dom2+GET', [('Host', 'www.mydom2.com')]) self.assertBody('You sent dom2 GET') - self.getPage('/vmethod', [('Host', 'www.mydom3.com')], method='POST', - body='value=dom3+POST') + self.getPage( + '/vmethod', + [('Host', 'www.mydom3.com')], + method='POST', + body='value=dom3+POST', + ) self.assertBody('You sent dom3 POST') self.getPage('/vmethod/pos', [('Host', 'www.mydom3.com')]) self.assertBody('You sent pos') diff --git a/cherrypy/test/test_wsgi_ns.py b/cherrypy/test/test_wsgi_ns.py index 3545724c5..0ca8a4e93 100644 --- a/cherrypy/test/test_wsgi_ns.py +++ b/cherrypy/test/test_wsgi_ns.py @@ -3,12 +3,9 @@ class WSGI_Namespace_Test(helper.CPWebCase): - @staticmethod def setup_server(): - class WSGIResponse(object): - def __init__(self, appresults): self.appresults = appresults self.iter = iter(appresults) @@ -27,7 +24,6 @@ def close(self): self.appresults.close() class ChangeCase(object): - def __init__(self, app, to=None): self.app = app self.to = to @@ -36,16 +32,15 @@ def __call__(self, environ, start_response): res = self.app(environ, start_response) class CaseResults(WSGIResponse): - def next(this): return getattr(this.iter.next(), self.to)() def __next__(this): return getattr(next(this.iter), self.to)() + return CaseResults(res) class Replacer(object): - def __init__(self, app, map={}): self.app = app self.map = map @@ -54,7 +49,6 @@ def __call__(self, environ, start_response): res = self.app(environ, start_response) class ReplaceResults(WSGIResponse): - def next(this): line = this.iter.next() for k, v in self.map.iteritems(): @@ -66,18 +60,18 @@ def __next__(this): for k, v in self.map.items(): line = line.replace(k, v) return line + return ReplaceResults(res) class Root(object): - @cherrypy.expose def index(self): return 'HellO WoRlD!' - root_conf = {'wsgi.pipeline': [('replace', Replacer)], - 'wsgi.replace.map': {b'L': b'X', - b'l': b'r'}, - } + root_conf = { + 'wsgi.pipeline': [('replace', Replacer)], + 'wsgi.replace.map': {b'L': b'X', b'l': b'r'}, + } app = cherrypy.Application(Root()) app.wsgiapp.pipeline.append(('changecase', ChangeCase)) diff --git a/cherrypy/test/test_wsgi_unix_socket.py b/cherrypy/test/test_wsgi_unix_socket.py index 32a3fca85..c7d519d43 100644 --- a/cherrypy/test/test_wsgi_unix_socket.py +++ b/cherrypy/test/test_wsgi_unix_socket.py @@ -52,12 +52,12 @@ class WSGI_UnixSocket_Test(helper.CPWebCase): It exercises the config option `server.socket_file`. """ + HTTP_CONN = USocketHTTPConnection(USOCKET_PATH) @staticmethod def setup_server(): class Root(object): - @cherrypy.expose def index(self): return 'Test OK' @@ -66,9 +66,7 @@ def index(self): def error(self): raise Exception('Invalid page') - config = { - 'server.socket_file': USOCKET_PATH - } + config = {'server.socket_file': USOCKET_PATH} cherrypy.config.update(config) cherrypy.tree.mount(Root()) diff --git a/cherrypy/test/test_wsgi_vhost.py b/cherrypy/test/test_wsgi_vhost.py index 2b6e5ba90..f78edc83d 100644 --- a/cherrypy/test/test_wsgi_vhost.py +++ b/cherrypy/test/test_wsgi_vhost.py @@ -3,12 +3,9 @@ class WSGI_VirtualHost_Test(helper.CPWebCase): - @staticmethod def setup_server(): - class ClassOfRoot(object): - def __init__(self, name): self.name = name @@ -31,5 +28,7 @@ def test_welcome(self): for year in range(1997, 2008): self.getPage( - '/', headers=[('Host', 'www.classof%s.example' % year)]) + '/', + headers=[('Host', 'www.classof%s.example' % year)], + ) self.assertBody('Welcome to the Class of %s website!' % year) diff --git a/cherrypy/test/test_wsgiapps.py b/cherrypy/test/test_wsgiapps.py index 1b3bf28fa..0cc7f07d2 100644 --- a/cherrypy/test/test_wsgiapps.py +++ b/cherrypy/test/test_wsgiapps.py @@ -6,16 +6,16 @@ class WSGIGraftTests(helper.CPWebCase): - @staticmethod def setup_server(): - def test_app(environ, start_response): status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) - output = ['Hello, world!\n', - 'This is a wsgi app running within CherryPy!\n\n'] + output = [ + 'Hello, world!\n', + 'This is a wsgi app running within CherryPy!\n\n', + ] keys = list(environ.keys()) keys.sort() for k in keys: @@ -27,11 +27,14 @@ def test_empty_string_app(environ, start_response): response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [ - b'Hello', b'', b' ', b'', b'world', + b'Hello', + b'', + b' ', + b'', + b'world', ] class WSGIResponse(object): - def __init__(self, appresults): self.appresults = appresults self.iter = iter(appresults) @@ -40,9 +43,11 @@ def __iter__(self): return self if sys.version_info >= (3, 0): + def __next__(self): return next(self.iter) else: + def next(self): return self.iter.next() @@ -51,7 +56,6 @@ def close(self): self.appresults.close() class ReversingMiddleware(object): - def __init__(self, app): self.app = app @@ -59,13 +63,14 @@ def __call__(self, environ, start_response): results = app(environ, start_response) class Reverser(WSGIResponse): - if sys.version_info >= (3, 0): + def __next__(this): line = list(next(this.iter)) line.reverse() return bytes(line) else: + def next(this): line = list(this.iter.next()) line.reverse() @@ -74,7 +79,6 @@ def next(this): return Reverser(results) class Root: - @cherrypy.expose def index(self): return ntob("I'm a regular CherryPy page handler!") @@ -89,8 +93,8 @@ def index(self): app = cherrypy.Application(Root(), script_name=None) cherrypy.tree.graft(ReversingMiddleware(app), '/hosted/app2') - wsgi_output = '''Hello, world! -This is a wsgi app running within CherryPy!''' + wsgi_output = """Hello, world! +This is a wsgi app running within CherryPy!""" def test_01_standard_app(self): self.getPage('/') diff --git a/cherrypy/test/test_xmlrpc.py b/cherrypy/test/test_xmlrpc.py index 61fde8bb2..6aac68d04 100644 --- a/cherrypy/test/test_xmlrpc.py +++ b/cherrypy/test/test_xmlrpc.py @@ -1,10 +1,7 @@ import sys import socket -from xmlrpc.client import ( - DateTime, Fault, - ServerProxy, SafeTransport -) +from xmlrpc.client import DateTime, Fault, ServerProxy, SafeTransport import cherrypy from cherrypy import _cptools @@ -18,15 +15,12 @@ def setup_server(): - class Root: - @cherrypy.expose def index(self): return "I'm a standard index!" class XmlRpc(_cptools.XMLRPCController): - @cherrypy.expose def foo(self): return 'Hello world!' @@ -77,17 +71,21 @@ def test_returning_Fault(self): root = Root() root.xmlrpc = XmlRpc() - cherrypy.tree.mount(root, config={'/': { - 'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(), - 'tools.xmlrpc.allow_none': 0, - }}) + cherrypy.tree.mount( + root, + config={ + '/': { + 'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(), + 'tools.xmlrpc.allow_none': 0, + }, + }, + ) class XmlRpcTest(helper.CPWebCase): setup_server = staticmethod(setup_server) def testXmlRpc(self): - scheme = self.scheme if scheme == 'https': url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT) @@ -103,15 +101,21 @@ def testXmlRpc(self): self.assertEqual(proxy.return_single_item_list(), [42]) self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion') self.assertEqual(proxy.return_string(), 'here is a string') - self.assertEqual(proxy.return_tuple(), - list(('here', 'is', 1, 'tuple'))) + self.assertEqual( + proxy.return_tuple(), + list(('here', 'is', 1, 'tuple')), + ) self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2}) - self.assertEqual(proxy.return_composite(), - [{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']]) + self.assertEqual( + proxy.return_composite(), + [{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']], + ) self.assertEqual(proxy.return_int(), 42) self.assertEqual(proxy.return_float(), 3.14) - self.assertEqual(proxy.return_datetime(), - DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))) + self.assertEqual( + proxy.return_datetime(), + DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)), + ) self.assertEqual(proxy.return_boolean(), True) self.assertEqual(proxy.test_argument_passing(22), 22 * 2) @@ -121,8 +125,10 @@ def testXmlRpc(self): except Exception: x = sys.exc_info()[1] self.assertEqual(x.__class__, Fault) - self.assertEqual(x.faultString, ('unsupported operand type(s) ' - "for *: 'dict' and 'int'")) + self.assertEqual( + x.faultString, + ("unsupported operand type(s) for *: 'dict' and 'int'"), + ) else: self.fail('Expected xmlrpclib.Fault') @@ -133,8 +139,10 @@ def testXmlRpc(self): except Exception: x = sys.exc_info()[1] self.assertEqual(x.__class__, Fault) - self.assertEqual(x.faultString, - 'method "non_method" is not supported') + self.assertEqual( + x.faultString, + 'method "non_method" is not supported', + ) else: self.fail('Expected xmlrpclib.Fault') diff --git a/cherrypy/test/webtest.py b/cherrypy/test/webtest.py index f579bc4cd..715f85a6e 100644 --- a/cherrypy/test/webtest.py +++ b/cherrypy/test/webtest.py @@ -1,11 +1,16 @@ """A deprecated web app testing helper interface.""" + # for compatibility, expose cheroot webtest here import warnings from cheroot.test.webtest import ( # noqa interface, - WebCase, cleanHeaders, shb, openURL, - ServerError, server_error, + WebCase, + cleanHeaders, + shb, + openURL, + ServerError, + server_error, ) diff --git a/cherrypy/tutorial/__init__.py b/cherrypy/tutorial/__init__.py index d6b560567..bcefff75d 100644 --- a/cherrypy/tutorial/__init__.py +++ b/cherrypy/tutorial/__init__.py @@ -1,3 +1,4 @@ """A package with standalone tutorial modules.""" + # This is used in test_config to test unrepr of "from A import B" thing2 = object() diff --git a/cherrypy/tutorial/tut03_get_and_post.py b/cherrypy/tutorial/tut03_get_and_post.py index 3a50a5847..ab109be80 100644 --- a/cherrypy/tutorial/tut03_get_and_post.py +++ b/cherrypy/tutorial/tut03_get_and_post.py @@ -16,12 +16,12 @@ class WelcomePage: def index(self): """Produce HTTP response body of welcome app index URI.""" # Ask for the user's name. - return ''' + return """
What is your name? -
''' + """ @cherrypy.expose def greetUser(self, name=None): diff --git a/cherrypy/tutorial/tut04_complex_site.py b/cherrypy/tutorial/tut04_complex_site.py index ddba6d9ad..fc81ac919 100644 --- a/cherrypy/tutorial/tut04_complex_site.py +++ b/cherrypy/tutorial/tut04_complex_site.py @@ -16,14 +16,14 @@ class HomePage: @cherrypy.expose def index(self): """Produce HTTP response body of home page app index URI.""" - return ''' + return """

Hi, this is the home page! Check out the other fun stuff on this site:

''' + """ class JokePage: @@ -32,10 +32,10 @@ class JokePage: @cherrypy.expose def index(self): """Produce HTTP response body of joke page app index URI.""" - return ''' + return """

"In Python, how do you create a string of random characters?" -- "Read a Perl file!"

-

[Return]

''' +

[Return]

""" class LinksPage: @@ -55,7 +55,7 @@ def index(self): # As you can see, this object doesn't really care about its # absolute position in the site tree, since we use relative # links exclusively. - return ''' + return """

Here are some useful links:

    @@ -71,7 +71,7 @@ def index(self): links here.

    [Return]

    - ''' + """ class ExtraLinksPage: @@ -81,7 +81,7 @@ class ExtraLinksPage: def index(self): """Render extra useful links.""" # Note the relative link back to the Links page! - return ''' + return """

    Here are some extra useful links:

      @@ -89,7 +89,7 @@ def index(self):
    • CherryPy
    -

    [Return to links page]

    ''' +

    [Return to links page]

    """ # Of course we can also mount request handler objects right here! diff --git a/cherrypy/tutorial/tut05_derived_objects.py b/cherrypy/tutorial/tut05_derived_objects.py index 7c9e8d288..d4417bc26 100644 --- a/cherrypy/tutorial/tut05_derived_objects.py +++ b/cherrypy/tutorial/tut05_derived_objects.py @@ -20,21 +20,21 @@ class Page: def header(self): """Render HTML layout header.""" - return ''' + return """ %s

    %s

    - ''' % (self.title, self.title) + """ % (self.title, self.title) def footer(self): """Render HTML layout footer.""" - return ''' + return """ - ''' + """ # Note that header and footer don't get their exposed attributes # set to True. This isn't necessary since the user isn't supposed @@ -59,12 +59,16 @@ def index(self): """Produce HTTP response body of home page app index URI.""" # Note that we call the header and footer methods inherited # from the Page class! - return self.header() + ''' + return ( + self.header() + + """

    Isn't this exciting? There's another page, too!

    - ''' + self.footer() + """ + + self.footer() + ) class AnotherPage(Page): @@ -75,11 +79,15 @@ class AnotherPage(Page): @cherrypy.expose def index(self): """Produce HTTP response body of another page app index URI.""" - return self.header() + ''' + return ( + self.header() + + """

    And this is the amazing second page!

    - ''' + self.footer() + """ + + self.footer() + ) tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') diff --git a/cherrypy/tutorial/tut06_default_method.py b/cherrypy/tutorial/tut06_default_method.py index 8a07a90e8..33074979e 100644 --- a/cherrypy/tutorial/tut06_default_method.py +++ b/cherrypy/tutorial/tut06_default_method.py @@ -30,11 +30,11 @@ def index(self): # Since this is just a stupid little example, we'll simply # display a list of links to random, made-up users. In a real # application, this could be generated from a database result set. - return ''' + return """ Remi Delon
    Hendrik Mans
    Lorenzo Lamas
    - ''' + """ @cherrypy.expose def default(self, user): diff --git a/cherrypy/tutorial/tut07_sessions.py b/cherrypy/tutorial/tut07_sessions.py index 210439904..aefc42e31 100644 --- a/cherrypy/tutorial/tut07_sessions.py +++ b/cherrypy/tutorial/tut07_sessions.py @@ -28,10 +28,13 @@ def index(self): cherrypy.session['count'] = count # And display a silly hit count message! - return ''' + return ( + """ During your current session, you've viewed this page %s times! Your life is a patio of fun! - ''' % count + """ + % count + ) tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') diff --git a/cherrypy/tutorial/tut09_files.py b/cherrypy/tutorial/tut09_files.py index 23bca6a59..bc88815ed 100644 --- a/cherrypy/tutorial/tut09_files.py +++ b/cherrypy/tutorial/tut09_files.py @@ -95,8 +95,12 @@ def upload(self, myFile): def download(self): """Send file to the HTTP client accessing ``/download`` URI.""" path = os.path.join(absDir, 'pdf_file.pdf') - return static.serve_file(path, 'application/x-download', - 'attachment', os.path.basename(path)) + return static.serve_file( + path, + 'application/x-download', + 'attachment', + os.path.basename(path), + ) tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') diff --git a/cherrypy/tutorial/tut10_http_errors.py b/cherrypy/tutorial/tut10_http_errors.py index f982df627..3110004b7 100644 --- a/cherrypy/tutorial/tut10_http_errors.py +++ b/cherrypy/tutorial/tut10_http_errors.py @@ -19,8 +19,7 @@ class HTTPErrorDemo(object): """HTTP error representation app.""" # Set a custom response for 403 errors. - _cp_config = {'error_page.403': - os.path.join(curpath, 'custom_error.html')} + _cp_config = {'error_page.403': os.path.join(curpath, 'custom_error.html')} @cherrypy.expose def index(self): @@ -32,7 +31,8 @@ def index(self): else: trace = 'on' - return """ + return ( + """

    Toggle tracebacks %s

    Click me; I'm a broken link!

    @@ -51,7 +51,9 @@ def index(self):

    You can also set the response body when you raise an error.

    - """ % trace + """ + % trace + ) @cherrypy.expose def toggleTracebacks(self): @@ -72,9 +74,11 @@ def error(self, code): @cherrypy.expose def messageArg(self): """Respond with an HTTP 500 and a custom message.""" - message = ("If you construct an HTTPError with a 'message' " - 'argument, it wil be placed on the error page ' - '(underneath the status line by default).') + message = ( + "If you construct an HTTPError with a 'message' " + 'argument, it wil be placed on the error page ' + '(underneath the status line by default).' + ) raise cherrypy.HTTPError(500, message=message) diff --git a/conftest.py b/conftest.py index f75774e98..b2126c789 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,5 @@ """Test configuration for pytest.""" - collect_ignore = [ # imports win32api, so not viable on some systems 'cherrypy/process/win32.py', diff --git a/docs/conf.py b/docs/conf.py index 806ca3717..8a926aaa4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ def get_supported_pythons(classifiers): """Return min and max supported Python version from meta as tuples.""" PY_VER_CLASSIFIER = 'Programming Language :: Python :: ' vers = filter(lambda c: c.startswith(PY_VER_CLASSIFIER), classifiers) - vers = map(lambda c: c[len(PY_VER_CLASSIFIER):], vers) + vers = map(lambda c: c[len(PY_VER_CLASSIFIER) :], vers) vers = filter(lambda c: c[0].isdigit() and '.' in c, vers) vers = map(lambda c: tuple(map(int, c.split('.'))), vers) vers = sorted(vers) @@ -44,7 +44,8 @@ def get_supported_pythons(classifiers): prj_description = prj_meta['Description'] prj_py_ver_range = get_supported_pythons(prj_meta.get_all('Classifier')) prj_py_min_supported, prj_py_max_supported = map( - lambda v: '.'.join(map(str, v)), prj_py_ver_range + lambda v: '.'.join(map(str, v)), + prj_py_ver_range, ) project = prj_meta['Name'] @@ -80,7 +81,6 @@ def get_supported_pythons(classifiers): 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', - # Third-party extensions: 'sphinxcontrib.apidoc', 'rst.linker', @@ -166,10 +166,16 @@ def get_supported_pythons(classifiers): # Custom sidebar templates, maps document names to template names. html_sidebars = { 'index': [ - 'about.html', 'searchbox.html', 'navigation.html', 'python_2_eol.html', + 'about.html', + 'searchbox.html', + 'navigation.html', + 'python_2_eol.html', ], '**': [ - 'about.html', 'searchbox.html', 'navigation.html', 'python_2_eol.html', + 'about.html', + 'searchbox.html', + 'navigation.html', + 'python_2_eol.html', ], } diff --git a/setup.py b/setup.py index ca9342e5d..2c7f738ec 100644 --- a/setup.py +++ b/setup.py @@ -51,13 +51,15 @@ 'Docs: RTD': 'https://docs.cherrypy.dev', 'GitHub: issues': '{}/issues'.format(repo_url), 'GitHub: repo': repo_url, - 'Tidelift: funding': - 'https://tidelift.com/subscription/pkg/pypi-cherrypy' + 'Tidelift: funding': 'https://tidelift.com/subscription/pkg/pypi-' + 'cherrypy' '?utm_source=pypi-cherrypy&utm_medium=referral&utm_campaign=pypi', }, packages=[ - 'cherrypy', 'cherrypy.lib', - 'cherrypy.tutorial', 'cherrypy.test', + 'cherrypy', + 'cherrypy.lib', + 'cherrypy.tutorial', + 'cherrypy.test', 'cherrypy.process', 'cherrypy.scaffold', ], @@ -85,7 +87,6 @@ 'testing': [ # cherrypy.lib.gctools 'objgraph', - 'pytest>=5.3.5', 'pytest-cov', 'pytest-forked', @@ -99,7 +100,6 @@ # Enables memcached session support via `cherrypy[memcached_session]`: 'memcached_session': ['python-memcached>=1.58'], 'xcgi': ['flup'], - # https://docs.cherrypy.dev/en/latest/advanced.html?highlight=windows#windows-console-events ':sys_platform == "win32" and implementation_name == "cpython"' # pywin32 disabled while a build is unavailable. Ref #1920. diff --git a/tests/dist-check.py b/tests/dist-check.py index 2ed2f0d52..8434beab7 100644 --- a/tests/dist-check.py +++ b/tests/dist-check.py @@ -15,12 +15,14 @@ import pytest -@pytest.fixture(params=[ - 'favicon.ico', - 'scaffold/static/made_with_cherrypy_small.png', - 'tutorial/tutorial.conf', - 'tutorial/custom_error.html', -]) +@pytest.fixture( + params=[ + 'favicon.ico', + 'scaffold/static/made_with_cherrypy_small.png', + 'tutorial/tutorial.conf', + 'tutorial/custom_error.html', + ], +) def data_file_path(request): """Generate data file paths expected to be found in the package.""" return request.param @@ -32,8 +34,7 @@ def remove_paths_to_checkout(): to_remove = [ path for path in sys.path - if os.path.isdir(path) - and os.path.samefile(path, os.path.curdir) + if os.path.isdir(path) and os.path.samefile(path, os.path.curdir) ] print('Removing', to_remove) list(map(sys.path.remove, to_remove)) @@ -43,6 +44,7 @@ def remove_paths_to_checkout(): def test_data_files_installed(data_file_path): """Ensure data file paths are available in the installed package.""" import cherrypy + root = os.path.dirname(cherrypy.__file__) fn = os.path.join(root, data_file_path) assert os.path.exists(fn), fn From fcbe3d01c21b0332f0bd3adae196771373ee55d4 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Mon, 5 May 2025 02:16:44 +0200 Subject: [PATCH 294/322] =?UTF-8?q?=F0=9F=92=85=20Hide=20925123b6=20from?= =?UTF-8?q?=20git=20blame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 6276b6adb..b650caa02 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -80,3 +80,6 @@ deeb3ae64c8039d136dbe958b2301049d8dfae72 # Revert "👹 Feed the hobgoblins (delint)." d968aaf8043a81ce4a636268ba8a42cb717ca316 + +# 🤖 Apply Ruff formatting +925123b66b5a42e36dc2fe08aae6dd27c8c4bc5b From c3214a303ab7068a078a89a7ed3a15ef2b9bdf2d Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Mon, 5 May 2025 02:17:10 +0200 Subject: [PATCH 295/322] =?UTF-8?q?=F0=9F=A7=AA=20Integrate=20Ruff=20forma?= =?UTF-8?q?tter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 20 +++++++++++++++++++- .ruff.toml | 3 +++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4067985ca..f29efef10 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,6 +21,25 @@ repos: # Ref: https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules - --fix # NOTE: When `--fix` is used, linting should be before ruff-format +- repo: https://github.com/astral-sh/ruff-pre-commit.git + rev: v0.11.7 + hooks: + - id: ruff-format + alias: ruff-format-first-pass + name: ruff-format (first pass) + +- repo: https://github.com/asottile/add-trailing-comma.git + rev: v3.1.0 + hooks: + - id: add-trailing-comma + +- repo: https://github.com/astral-sh/ruff-pre-commit.git + rev: v0.11.7 + hooks: + - id: ruff-format + alias: ruff-format-second-pass + name: ruff-format (second pass) + - repo: https://github.com/python-jsonschema/check-jsonschema.git rev: 0.33.0 hooks: @@ -44,7 +63,6 @@ repos: - id: trailing-whitespace exclude: cherrypy/test/static/index.html - id: check-merge-conflict - - id: double-quote-string-fixer - id: end-of-file-fixer - id: name-tests-test include: cherrypy/test/ diff --git a/.ruff.toml b/.ruff.toml index 48e5362e0..dfdac3fdd 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -7,6 +7,9 @@ namespace-packages = [ "tests/", ] +[format] +quote-style = "single" + [lint] ignore = [ "CPY001", # Skip copyright notice requirement at top of files From fd03a4128e8cad739700b5a44bfb7e5df1858a7d Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sun, 11 May 2025 01:21:31 +0200 Subject: [PATCH 296/322] =?UTF-8?q?=F0=9F=A7=AA=20Make=20the=20GHA=20workf?= =?UTF-8?q?low=20sparse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index fc6ec4ed0..c8924b598 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -155,6 +155,7 @@ jobs: upstream-repository-id: ${{ env.UPSTREAM_REPOSITORY_ID }} publishing-to-testpypi-enabled: ${{ env.PUBLISHING_TO_TESTPYPI_ENABLED }} is-debug-mode: ${{ toJSON(runner.debug == '1') }} + steps: - name: Switch to using Python 3.11 by default uses: actions/setup-python@v5 From dca95489342f4bf7bf06fb75d40c58329946f982 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Wed, 14 May 2025 22:21:30 +0200 Subject: [PATCH 297/322] =?UTF-8?q?=F0=9F=A7=AA=20Stop=20running=20PyPy=20?= =?UTF-8?q?on=20*NIX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's not runnable currently. --- .github/workflows/ci-cd.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index c8924b598..1adc68cab 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -474,16 +474,16 @@ jobs: # NOTE: of the build deps to compile cryptography. # NOTE: They should be re-added once it's fixed. - runner-vm-os: windows-2022 - python-version: pypy-3.9 + python-version: pypy-3.11 - runner-vm-os: windows-2019 - python-version: pypy-3.9 + python-version: pypy-3.11 # NOTE: macOS PyPy jobs are excluded because installing cryptography # NOTE: needs openssl headers that aren't present at the moment. # TODO: Remove the exclusions once this is addressed. - - runner-vm-os: macos-11 - python-version: pypy-3.9 - - runner-vm-os: macos-12 - python-version: pypy-3.9 + - runner-vm-os: macos-15 + python-version: pypy-3.11 + - runner-vm-os: macos-13 + python-version: pypy-3.11 # yamllint disable rule:comments-indentation # include: [] # TODO: include TOXENV=cheroot-master # yamllint enable rule:comments-indentation From a695e4522f1b3959f430b312921c5b8540123d93 Mon Sep 17 00:00:00 2001 From: SOUBHIK KUMAR MITRA Date: Wed, 18 Jun 2025 17:54:03 +0530 Subject: [PATCH 298/322] Remove "Freely Distributable" license classifier As mentioned in the comments "License :: Freely Distributable" classifier is fine to drop as pypi.org/classifiers doesn't really specify semantics around this classifier + PEP 639 mandates getting rid of license-related classifiers in favor of SPDX, anyway. Also https://github.com/cherrypy/cherrypy/commit/cd06246519ab4aada9976ed43ab5aef694b6a59c per the commit it seems like BSD-3-clause classifier was added already and this got missed for the removal. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 2c7f738ec..9890614a4 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,6 @@ 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Intended Audience :: Developers', - 'License :: Freely Distributable', 'Operating System :: OS Independent', 'Framework :: CherryPy', 'License :: OSI Approved :: BSD License', From 76c0c24f6bf5676ec3264eaf0bd94fc92860f41d Mon Sep 17 00:00:00 2001 From: SOUBHIK KUMAR MITRA Date: Thu, 3 Jul 2025 07:10:38 +0530 Subject: [PATCH 299/322] Change occurance for cherrpy.org to cherrypy.dev As the title suggest this commit change the remaining occurance of www.cherrypy.org to www.cherrypy.dev accross the codebase as also mentioned in the https://github.com/python3statement/python3statement.github.io/pull/294#issue-2382318887, so further it stops redirecting users to the wrong domain. Also, with that noticed that cherrypy.org/wiki* has some deadlinks which if changed to the cherrypy.dev/wiki* crashes with a 404 so changed those links to the web.archive --- cherrypy/lib/auth_basic.py | 2 +- cherrypy/lib/auth_digest.py | 2 +- cherrypy/lib/profiler.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cherrypy/lib/auth_basic.py b/cherrypy/lib/auth_basic.py index 7b0ffcbd6..41f2eeea2 100644 --- a/cherrypy/lib/auth_basic.py +++ b/cherrypy/lib/auth_basic.py @@ -1,4 +1,4 @@ -# This file is part of CherryPy +# This file is part of CherryPy # -*- coding: utf-8 -*- # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 """HTTP Basic Authentication tool. diff --git a/cherrypy/lib/auth_digest.py b/cherrypy/lib/auth_digest.py index b7ad28c48..790bf96c9 100644 --- a/cherrypy/lib/auth_digest.py +++ b/cherrypy/lib/auth_digest.py @@ -1,4 +1,4 @@ -# This file is part of CherryPy +# This file is part of CherryPy # -*- coding: utf-8 -*- # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 """HTTP Digest Authentication tool. diff --git a/cherrypy/lib/profiler.py b/cherrypy/lib/profiler.py index dc2f9ffee..f0af29e57 100644 --- a/cherrypy/lib/profiler.py +++ b/cherrypy/lib/profiler.py @@ -190,7 +190,7 @@ def __init__(self, nextapp, path=None, aggregate=False): 'Your installation of Python does not have a profile ' "module. If you're on Debian, try " '`sudo apt-get install python-profiler`. ' - 'See http://www.cherrypy.org/wiki/ProfilingOnDebian ' + 'See https://web.archive.org/web/20111016122921/http://www.cherrypy.org/wiki/ProfilingOnDebian ' 'for details.' ) warnings.warn(msg) @@ -221,7 +221,7 @@ def serve(path=None, port=8080): 'Your installation of Python does not have a profile module. ' "If you're on Debian, try " '`sudo apt-get install python-profiler`. ' - 'See http://www.cherrypy.org/wiki/ProfilingOnDebian ' + 'See https://web.archive.org/web/20111016122921/http://www.cherrypy.org/wiki/ProfilingOnDebian ' 'for details.' ) warnings.warn(msg) From 7443e18d6bbcf4a884e05b8c751f512690f26643 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sat, 30 Aug 2025 15:13:18 +0200 Subject: [PATCH 300/322] =?UTF-8?q?=F0=9F=A7=AA=20Bump=20`reusable-tox.yml?= =?UTF-8?q?`=20to=20208490c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 1adc68cab..308ff0625 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -325,7 +325,7 @@ jobs: needs: - pre-setup # transitive, for accessing settings - uses: tox-dev/workflow/.github/workflows/reusable-tox.yml@89de3c6be3cd179adf71e28aa4ac5bef60804209 # yamllint disable-line rule:line-length + uses: tox-dev/workflow/.github/workflows/reusable-tox.yml@208490c75f7f6b81e2698cc959f24d264c462d57 # yamllint disable-line rule:line-length with: cache-key-for-dependency-files: >- ${{ needs.pre-setup.outputs.cache-key-for-dep-files }} @@ -371,7 +371,7 @@ jobs: - false fail-fast: false - uses: tox-dev/workflow/.github/workflows/reusable-tox.yml@89de3c6be3cd179adf71e28aa4ac5bef60804209 # yamllint disable-line rule:line-length + uses: tox-dev/workflow/.github/workflows/reusable-tox.yml@208490c75f7f6b81e2698cc959f24d264c462d57 # yamllint disable-line rule:line-length with: built-wheel-names: >- ${{ @@ -488,7 +488,7 @@ jobs: # include: [] # TODO: include TOXENV=cheroot-master # yamllint enable rule:comments-indentation - uses: tox-dev/workflow/.github/workflows/reusable-tox.yml@89de3c6be3cd179adf71e28aa4ac5bef60804209 # yamllint disable-line rule:line-length + uses: tox-dev/workflow/.github/workflows/reusable-tox.yml@208490c75f7f6b81e2698cc959f24d264c462d57 # yamllint disable-line rule:line-length with: built-wheel-names: >- ${{ needs.pre-setup.outputs.wheel-artifact-name }} From 1f803987bc46978f3b4ee46e529f5e7743a1fde3 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sat, 30 Aug 2025 15:15:08 +0200 Subject: [PATCH 301/322] Keep `setuptools-scm` in sync across CI --- .github/workflows/ci-cd.yml | 1 + .../lock-files/dist-build-constraints.txt | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 dependencies/lock-files/dist-build-constraints.txt diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 308ff0625..298d73f86 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -235,6 +235,7 @@ jobs: pip install --user setuptools-scm + --constraint=dependencies/lock-files/dist-build-constraints.txt shell: bash - name: Set the current dist version from Git if: steps.request-check.outputs.release-requested != 'true' diff --git a/dependencies/lock-files/dist-build-constraints.txt b/dependencies/lock-files/dist-build-constraints.txt new file mode 100644 index 000000000..6ef52ad7c --- /dev/null +++ b/dependencies/lock-files/dist-build-constraints.txt @@ -0,0 +1,16 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# tox r -e pip-compile-build-lock -- +# +packaging==24.1 + # via setuptools-scm +setuptools-scm==8.1.0 + # via awx-plugins-core (pyproject.toml::build-system.requires) + +# The following packages are considered to be unsafe in a requirements file: +setuptools==73.0.0 + # via + # awx-plugins-core (pyproject.toml::build-system.requires) + # setuptools-scm From 0f364c0dda3f84d032cd83a77d1fbd81c85ee248 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Mon, 22 Sep 2025 13:04:51 +0200 Subject: [PATCH 302/322] =?UTF-8?q?=F0=9F=A7=AA=20Ensure=20SLSA=20runs=20i?= =?UTF-8?q?n=20YOLO=20mode=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 298d73f86..bdcf063b5 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -748,6 +748,9 @@ jobs: - build - post-release-repo-update - pre-setup # transitive, for accessing settings + if: >- + always() + && needs.post-release-repo-update.result == 'success' permissions: actions: read From ef7f18b4373386b3ac2bed3adffc261476607744 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 14 Nov 2025 04:23:49 +0100 Subject: [PATCH 303/322] =?UTF-8?q?=F0=9F=A7=AA=F0=9F=92=85=20Add=20lables?= =?UTF-8?q?=20to=20workflow=20run=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index bdcf063b5..a4c856a1e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -93,9 +93,9 @@ run-name: >- || '' }} ${{ - github.event.pull_request.number && 'PR' || '' + github.event.pull_request.number && '🔀 PR' || '' }}${{ - !github.event.pull_request.number && 'Commit' || '' + !github.event.pull_request.number && '🌱 Commit' || '' }} ${{ github.event.pull_request.number || github.sha }} triggered by: ${{ github.event_name }} of ${{ From be90d2af58c521fba42b3fb034fab9eee609ebc5 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 14 Nov 2025 04:26:14 +0100 Subject: [PATCH 304/322] =?UTF-8?q?=F0=9F=A7=AA=20Correct=20`paths-ignore`?= =?UTF-8?q?=20setting=20spelling=20@=20GHA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index a4c856a1e..946fc54e6 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -13,7 +13,7 @@ on: - patchback/backports/** # Patchback always creates PRs - pre-commit-ci-update-config # pre-commit.ci always creates a PR pull_request: - ignore-paths: # changes to the cron workflow are triggered through it + paths-ignore: # changes to the cron workflow are triggered through it - .github/workflows/scheduled-runs.yml workflow_call: # a way to embed the main tests workflow_dispatch: From 9a272ecce67c6847aaa054fe091cf6553054d1a9 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 14 Nov 2025 04:34:06 +0100 Subject: [PATCH 305/322] =?UTF-8?q?=F0=9F=A7=AA=F0=9F=92=85=20Indent=20nes?= =?UTF-8?q?ted=20build=20cmd=20@=20tox=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index a1beb6207..65f29d921 100644 --- a/tox.ini +++ b/tox.ini @@ -222,9 +222,9 @@ commands = {[python-cli-options]byte-errors} \ {[python-cli-options]some-isolation} \ -m build \ - --outdir '{env:PEP517_OUT_DIR}{/}' \ - {posargs:{env:PEP517_BUILD_ARGS:}} \ - '{toxinidir}' + --outdir '{env:PEP517_OUT_DIR}{/}' \ + {posargs:{env:PEP517_BUILD_ARGS:}} \ + '{toxinidir}' commands_post = From 6a25f7323a475e4bcdf9156039979cc481b18ccd Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 14 Nov 2025 04:37:13 +0100 Subject: [PATCH 306/322] =?UTF-8?q?=F0=9F=A7=AA=20Ensure=20tox=20uses=20th?= =?UTF-8?q?e=20same=20pkg=20build=20pins=20as=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a follow-up for 1f80398. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 65f29d921..20b291111 100644 --- a/tox.ini +++ b/tox.ini @@ -114,6 +114,7 @@ whitelist_externals = mkdir [dists] setenv = + PIP_CONSTRAINT = {toxinidir}{/}dependencies{/}lock-files{/}dist-build-constraints.txt PEP517_OUT_DIR = {env:PEP517_OUT_DIR:{toxinidir}{/}dist} From 3f69f2088241f5b90e1f87bcc15156696ebc9aa0 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 14 Nov 2025 04:39:17 +0100 Subject: [PATCH 307/322] =?UTF-8?q?=F0=9F=A7=AA=20Use=20tox's=20in-venv=20?= =?UTF-8?q?Python=20in=20`exec`=20@=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 946fc54e6..49d6aecbd 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -395,7 +395,10 @@ jobs: post-toxenv-preparation-command: >- ${{ matrix.toxenv == 'pre-commit' - && 'python -Im pre_commit install-hooks' + && format( + '$(pwd)/.tox/{0}/bin/python -Im pre_commit install-hooks', + matrix.toxenv + ) || '' }} python-version: >- From 9afb286c3b24a29a17407ce62954e37b2f3003e0 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 14 Nov 2025 04:40:30 +0100 Subject: [PATCH 308/322] =?UTF-8?q?=F0=9F=A7=AA=20Add=20experimental=20Pyt?= =?UTF-8?q?hon=203.14=20jobs=20to=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They are allowed to fail since it's not yet stable. --- .github/workflows/ci-cd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 49d6aecbd..9078d0519 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -461,6 +461,7 @@ jobs: - 3.11 - >- 3.10 + - ~3.14.0-0 runner-vm-os: - ubuntu-24.04-arm - ubuntu-24.04 From 01cd656835b7d9a5b6697eafc495c960b5a13426 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 14 Nov 2025 04:41:20 +0100 Subject: [PATCH 309/322] =?UTF-8?q?=F0=9F=A7=AA=20Run=20linters=20under=20?= =?UTF-8?q?Python=203.14=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9078d0519..4d62da8cc 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -359,7 +359,10 @@ jobs: runner-vm-os: - ubuntu-latest python-version: - - 3.11 + - |- + 3.11 + 3.12 + 3.14 toxenv: - pre-commit - metadata-validation From b1c17558fd6a79a519319d0c7850791f56c0e9ba Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 14 Nov 2025 04:43:22 +0100 Subject: [PATCH 310/322] =?UTF-8?q?=F0=9F=A7=AA=20Replace=20Windows=202019?= =?UTF-8?q?=20w/=202025=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 4d62da8cc..8c4f73c1b 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -469,21 +469,21 @@ jobs: - ubuntu-24.04-arm - ubuntu-24.04 - macos-15 - - windows-2022 + - windows-2025 - ubuntu-22.04 - macos-13 - - windows-2019 + - windows-2022 toxenv: - py xfail: - false exclude: - # NOTE: Windows PyPy 3.9 jobs are excluded because of the lack + # NOTE: Windows PyPy 3.11 jobs are excluded because of the lack # NOTE: of the build deps to compile cryptography. # NOTE: They should be re-added once it's fixed. - - runner-vm-os: windows-2022 + - runner-vm-os: windows-2025 python-version: pypy-3.11 - - runner-vm-os: windows-2019 + - runner-vm-os: windows-2022 python-version: pypy-3.11 # NOTE: macOS PyPy jobs are excluded because installing cryptography # NOTE: needs openssl headers that aren't present at the moment. From 7c6198e37cb81dabee4136b42ef6bda3215ed12a Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 14 Nov 2025 04:48:51 +0100 Subject: [PATCH 311/322] =?UTF-8?q?=F0=9F=A7=AA=20Correct=20case=20in=20th?= =?UTF-8?q?e=20project=20name=20var=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 8c4f73c1b..ed9e07c0e 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -63,7 +63,7 @@ env: PIP_NO_PYTHON_VERSION_WARNING: 1 # Hide "this Python is deprecated" message PIP_NO_WARN_SCRIPT_LOCATION: 1 # Hide "script dir is not in $PATH" message PRE_COMMIT_COLOR: always - PROJECT_NAME: cherrypy + PROJECT_NAME: CherryPy PUBLISHING_TO_TESTPYPI_ENABLED: true PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest` PYTHONIOENCODING: utf-8 @@ -286,7 +286,11 @@ jobs: FILE_APPEND_MODE = 'a' whl_file_prj_base_name = '${{ env.PROJECT_NAME }}'.replace('-', '_') - sdist_file_prj_base_name = whl_file_prj_base_name.replace('.', '_') + sdist_file_prj_base_name = ( + whl_file_prj_base_name. + replace('.', '_') + .lower() + ) with Path(environ['GITHUB_OUTPUT']).open( mode=FILE_APPEND_MODE, From 33239f278d43034bf368258b0ff1e667d3709a4f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sun, 16 Nov 2025 04:33:22 +0100 Subject: [PATCH 312/322] =?UTF-8?q?=F0=9F=A7=AA=20Use=20cleaner=20`github.?= =?UTF-8?q?ref=5F(type|name)`=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index ed9e07c0e..d545f2337 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -168,9 +168,8 @@ jobs: id: untagged-check if: >- github.event_name == 'push' && - github.ref == format( - 'refs/heads/{0}', github.event.repository.default_branch - ) + github.ref_type == 'branch' && + github.ref_name == github.event.repository.default_branch run: | from os import environ from pathlib import Path From 124ee7e75d2cb972ba3e9543be575bbb49a7b84d Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sun, 16 Nov 2025 04:34:32 +0100 Subject: [PATCH 313/322] =?UTF-8?q?=F0=9F=A7=AA=20Normalize=20period=20pla?= =?UTF-8?q?cement=20@=20GHA=20Python?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index d545f2337..f154c0905 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -287,8 +287,8 @@ jobs: whl_file_prj_base_name = '${{ env.PROJECT_NAME }}'.replace('-', '_') sdist_file_prj_base_name = ( whl_file_prj_base_name. - replace('.', '_') - .lower() + replace('.', '_'). + lower() ) with Path(environ['GITHUB_OUTPUT']).open( From 771a2e92b73a16be7eeea4e43b2b26af37fb5abc Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sun, 16 Nov 2025 04:35:03 +0100 Subject: [PATCH 314/322] =?UTF-8?q?=F0=9F=A7=AA=20Source=20project=20name?= =?UTF-8?q?=20via=20PEP=20621=20in=20CI/CD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index f154c0905..17e7c7372 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -63,7 +63,6 @@ env: PIP_NO_PYTHON_VERSION_WARNING: 1 # Hide "this Python is deprecated" message PIP_NO_WARN_SCRIPT_LOCATION: 1 # Hide "script dir is not in $PATH" message PRE_COMMIT_COLOR: always - PROJECT_NAME: CherryPy PUBLISHING_TO_TESTPYPI_ENABLED: true PY_COLORS: 1 # Recognized by the `py` package, dependency of `pytest` PYTHONIOENCODING: utf-8 @@ -133,6 +132,7 @@ jobs: && github.event.inputs.release-version || steps.scm-version.outputs.dist-version }} + project-name: ${{ steps.metadata.outputs.project-name }} is-untagged-devel: >- ${{ steps.untagged-check.outputs.is-untagged-devel || false }} release-requested: >- @@ -194,8 +194,6 @@ jobs: ) as outputs_file: print('release-requested=true', file=outputs_file) - name: Check out src from Git - if: >- - steps.request-check.outputs.release-requested != 'true' uses: actions/checkout@v4 with: fetch-depth: >- @@ -204,6 +202,23 @@ jobs: && 1 || 0 }} ref: ${{ github.event.inputs.release-committish }} + - name: Scan static PEP 621 core packaging metadata + id: metadata + run: | + from os import environ + from pathlib import Path + from tomllib import loads as parse_toml_from_string + + FILE_APPEND_MODE = 'a' + + pyproject_toml_txt = Path('pyproject.toml').read_text() + metadata = parse_toml_from_string(pyproject_toml_txt)['project'] + project_name = metadata["name"] + + with Path(environ['GITHUB_OUTPUT']).open( + mode=FILE_APPEND_MODE, + ) as outputs_file: + print(f'project-name={project_name}', file=outputs_file) - name: >- Calculate dependency files' combined hash value for use in the cache key @@ -278,13 +293,15 @@ jobs: ) - name: Set the expected dist artifact names id: artifact-name + env: + PROJECT_NAME: ${{ steps.metadata.outputs.project-name }} run: | from os import environ from pathlib import Path FILE_APPEND_MODE = 'a' - whl_file_prj_base_name = '${{ env.PROJECT_NAME }}'.replace('-', '_') + whl_file_prj_base_name = environ['PROJECT_NAME'].replace('-', '_') sdist_file_prj_base_name = ( whl_file_prj_base_name. replace('.', '_'). @@ -596,7 +613,9 @@ jobs: environment: name: pypi url: >- - https://pypi.org/project/${{ env.PROJECT_NAME }}/${{ + https://pypi.org/project/${{ + needs.pre-setup.outputs.project-name + }}/${{ needs.pre-setup.outputs.dist-version }} @@ -641,7 +660,9 @@ jobs: environment: name: testpypi url: >- - https://test.pypi.org/project/${{ env.PROJECT_NAME }}/${{ + https://test.pypi.org/project/${{ + needs.pre-setup.outputs.project-name + }}/${{ needs.pre-setup.outputs.dist-version }} @@ -726,7 +747,7 @@ jobs: git tag -m '${{ needs.pre-setup.outputs.git-tag }}' -m 'Published at https://pypi.org/project/${{ - env.PROJECT_NAME + needs.pre-setup.outputs.project-name }}/${{ needs.pre-setup.outputs.dist-version }}' @@ -882,7 +903,7 @@ jobs: echo | tee -a release-notes.md echo | tee -a release-notes.md echo '📦 PyPI page: https://pypi.org/project/${{ - env.PROJECT_NAME + needs.pre-setup.outputs.project-name }}/${{ needs.pre-setup.outputs.dist-version }}' | tee -a release-notes.md From 05dd9edf67acdfa69b1f595c1625d95678c88f5b Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 20 Nov 2025 00:08:14 +0100 Subject: [PATCH 315/322] Ensure `cherrypy.lib.profiler` has fitting lines --- cherrypy/lib/profiler.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cherrypy/lib/profiler.py b/cherrypy/lib/profiler.py index f0af29e57..659df56cb 100644 --- a/cherrypy/lib/profiler.py +++ b/cherrypy/lib/profiler.py @@ -190,8 +190,8 @@ def __init__(self, nextapp, path=None, aggregate=False): 'Your installation of Python does not have a profile ' "module. If you're on Debian, try " '`sudo apt-get install python-profiler`. ' - 'See https://web.archive.org/web/20111016122921/http://www.cherrypy.org/wiki/ProfilingOnDebian ' - 'for details.' + 'See https://web.archive.org/web/20111016122921/' + 'http://www.cherrypy.org/wiki/ProfilingOnDebian for details.' ) warnings.warn(msg) @@ -221,8 +221,8 @@ def serve(path=None, port=8080): 'Your installation of Python does not have a profile module. ' "If you're on Debian, try " '`sudo apt-get install python-profiler`. ' - 'See https://web.archive.org/web/20111016122921/http://www.cherrypy.org/wiki/ProfilingOnDebian ' - 'for details.' + 'See https://web.archive.org/web/20111016122921/' + 'http://www.cherrypy.org/wiki/ProfilingOnDebian for details.' ) warnings.warn(msg) From cea2627a8d2b23a11ab19173631ed89f11e0c164 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 20 Nov 2025 00:10:49 +0100 Subject: [PATCH 316/322] =?UTF-8?q?=F0=9F=92=85=20Make=20`pyproject.toml`?= =?UTF-8?q?=20indents=20two-space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 88dbae31c..d71f1f920 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] requires = [ - # Essentials - "setuptools >= 45", + # Essentials + "setuptools >= 45", - # Plugins - "setuptools_scm[toml] >= 7", + # Plugins + "setuptools_scm[toml] >= 7", ] build-backend = "setuptools.build_meta" From cfe7c0d223cb1fd756c73c24e077470c5f78b4d0 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 20 Nov 2025 00:11:27 +0100 Subject: [PATCH 317/322] Drop `toml` extra from `setuptools-scm` build dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d71f1f920..ed86998f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires = [ "setuptools >= 45", # Plugins - "setuptools_scm[toml] >= 7", + "setuptools-scm >= 7.0.0", ] build-backend = "setuptools.build_meta" From 34304e8ad209e4db6a1acd68d6065c26d0bc602e Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 20 Nov 2025 00:45:26 +0100 Subject: [PATCH 318/322] =?UTF-8?q?=F0=9F=93=A6=20Convert=20core=20packagi?= =?UTF-8?q?n=20metadata=20to=20PEP=20621?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 116 ++++++++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 4 -- setup.py | 116 ------------------------------------------------- 3 files changed, 115 insertions(+), 121 deletions(-) delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index ed86998f4..247576ff0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,125 @@ [build-system] requires = [ # Essentials - "setuptools >= 45", + "setuptools >= 61.2", # Plugins "setuptools-scm >= 7.0.0", ] build-backend = "setuptools.build_meta" +[project] +name = "CherryPy" +description = "Object-Oriented HTTP framework" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Framework :: CherryPy", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: Implementation", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: Jython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Internet :: WWW/HTTP :: HTTP Servers", + "Topic :: Internet :: WWW/HTTP :: WSGI", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Server", + "Topic :: Software Development :: Libraries :: Application Frameworks", +] +requires-python = ">= 3.9" +dependencies = [ + "cheroot >= 8.2.1", + "portend >= 2.1.1", + "more_itertools", + "filelock", + "jaraco.collections", + # pywin32 disabled while a build is unavailable. Ref #1920. + 'pywin32 >= 227; sys_platform == "win32" and implementation_name == "cpython" and python_version < "3.10"', +] +dynamic = [ + "version", +] + +[[project.authors]] +name = "CherryPy Team" +email = "team@cherrypy.dev" + +[project.urls] +Homepage = "https://cherrypy.dev" +"Chat: Matrix" = "https://matrix.to/#/#cherrypy-space:matrix.org" +"CI: GitHub" = "https://github.com/cherrypy/cherrypy/actions" +"Docs: RTD" = "https://docs.cherrypy.dev" +"GitHub: issues" = "https://github.com/cherrypy/cherrypy/issues" +"GitHub: repo" = "https://github.com/cherrypy/cherrypy" +"Tidelift: funding" = "https://tidelift.com/subscription/pkg/pypi-cherrypy?utm_source=pypi-cherrypy&utm_medium=referral&utm_campaign=pypi" + +[project.readme] +file = "README.rst" +content-type = "text/x-rst" + +[project.optional-dependencies] +docs = [ + "sphinx", + "docutils", + "alabaster", + "sphinxcontrib-apidoc >= 0.3.0", + "rst.linker >= 1.11", + "jaraco.packaging >= 3.2", +] +json = [ + "simplejson", +] +routes_dispatcher = [ + "routes >= 2.3.1", +] +ssl = [ + "pyOpenSSL", +] +testing = [ + "objgraph", # cherrypy.lib.gctools + "pytest >= 5.3.5", + "pytest-cov", + "pytest-forked", + "pytest-rerunfailures", + "pytest-sugar", + "path", + "requests_toolbelt", + "pytest-services >= 2", + "setuptools", +] +memcached_session = [ + # Enables memcached session support via `cherrypy[memcached_session]` + "python-memcached >= 1.58", +] +xcgi = [ + "flup", +] + +[project.scripts] +cherryd = "cherrypy.__main__:run" + +[tool.setuptools] +license-files = [ + "LICENSE.md", +] +packages = [ + "cherrypy", + "cherrypy.lib", + "cherrypy.tutorial", + "cherrypy.test", + "cherrypy.process", + "cherrypy.scaffold", +] + [tool.setuptools_scm] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 33310ef95..000000000 --- a/setup.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[metadata] -license_file = LICENSE.md -long_description = file:README.rst -long_description_content_type = text/x-rst diff --git a/setup.py b/setup.py deleted file mode 100644 index 9890614a4..000000000 --- a/setup.py +++ /dev/null @@ -1,116 +0,0 @@ -#! /usr/bin/env python -"""CherryPy package setuptools installer.""" - -import setuptools - - -name = 'CherryPy' -repo_slug = 'cherrypy/{}'.format(name.lower()) -repo_url = 'https://github.com/{}'.format(repo_slug) - - -params = dict( - name=name, - use_scm_version=True, - description='Object-Oriented HTTP framework', - author='CherryPy Team', - author_email='team@cherrypy.dev', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'Framework :: CherryPy', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', - 'Programming Language :: Python :: Implementation', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: Jython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Internet :: WWW/HTTP', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Internet :: WWW/HTTP :: HTTP Servers', - 'Topic :: Internet :: WWW/HTTP :: WSGI', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application', - 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server', - 'Topic :: Software Development :: Libraries :: Application Frameworks', - ], - url='https://www.cherrypy.dev', - project_urls={ - 'CI: AppVeyor': 'https://ci.appveyor.com/project/{}'.format(repo_slug), - 'CI: Travis': 'https://travis-ci.org/{}'.format(repo_slug), - 'CI: Circle': 'https://circleci.com/gh/{}'.format(repo_slug), - 'CI: GitHub': 'https://github.com/{}/actions'.format(repo_slug), - 'Docs: RTD': 'https://docs.cherrypy.dev', - 'GitHub: issues': '{}/issues'.format(repo_url), - 'GitHub: repo': repo_url, - 'Tidelift: funding': 'https://tidelift.com/subscription/pkg/pypi-' - 'cherrypy' - '?utm_source=pypi-cherrypy&utm_medium=referral&utm_campaign=pypi', - }, - packages=[ - 'cherrypy', - 'cherrypy.lib', - 'cherrypy.tutorial', - 'cherrypy.test', - 'cherrypy.process', - 'cherrypy.scaffold', - ], - entry_points={'console_scripts': ['cherryd = cherrypy.__main__:run']}, - include_package_data=True, - install_requires=[ - 'cheroot>=8.2.1', - 'portend>=2.1.1', - 'more_itertools', - 'filelock', - 'jaraco.collections', - ], - extras_require={ - 'docs': [ - 'sphinx', - 'docutils', - 'alabaster', - 'sphinxcontrib-apidoc>=0.3.0', - 'rst.linker>=1.11', - 'jaraco.packaging>=3.2', - ], - 'json': ['simplejson'], - 'routes_dispatcher': ['routes>=2.3.1'], - 'ssl': ['pyOpenSSL'], - 'testing': [ - # cherrypy.lib.gctools - 'objgraph', - 'pytest>=5.3.5', - 'pytest-cov', - 'pytest-forked', - 'pytest-rerunfailures', - 'pytest-sugar', - 'path', - 'requests_toolbelt', - 'pytest-services>=2', - 'setuptools', - ], - # Enables memcached session support via `cherrypy[memcached_session]`: - 'memcached_session': ['python-memcached>=1.58'], - 'xcgi': ['flup'], - # https://docs.cherrypy.dev/en/latest/advanced.html?highlight=windows#windows-console-events - ':sys_platform == "win32" and implementation_name == "cpython"' - # pywin32 disabled while a build is unavailable. Ref #1920. - ' and python_version < "3.10"': [ - 'pywin32 >= 227', - ], - }, - setup_requires=[ - 'setuptools_scm', - ], - python_requires='>= 3.9', -) - - -__name__ == '__main__' and setuptools.setup(**params) From 8a712fdc3d43a0dce8471da6f3c275dbeffb23c3 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 20 Nov 2025 01:12:24 +0100 Subject: [PATCH 319/322] =?UTF-8?q?=F0=9F=A7=AA=20Drop=20references=20to?= =?UTF-8?q?=20unused=20CIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .appveyor.yml | 47 ---------- .circleci/config.yml | 56 ----------- .travis.yml | 174 ----------------------------------- .zuul.yaml | 78 ---------------- README.rst | 9 -- cherrypy/test/test_states.py | 6 -- tox.ini | 6 -- 7 files changed, 376 deletions(-) delete mode 100644 .appveyor.yml delete mode 100644 .circleci/config.yml delete mode 100644 .travis.yml delete mode 100644 .zuul.yaml diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 5c3add71c..000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,47 +0,0 @@ -# yamllint disable rule:line-length ---- - -environment: - matrix: - - PYTHON: "C:\\Python37-x64" - - PYTHON: "C:\\Python36-x64" - -init: -- "chcp 65001" -- ps: >- - if($env:APPVEYOR_RDP_DEBUG -eq 'True') { - iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) - } - -install: -# symlink python from a directory with a space -- "mklink /d \"C:\\Program Files\\Python\" %PYTHON%" -- "SET PYTHON=\"C:\\Program Files\\Python\"" -- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" -- "python -m pip install tox" -- "python -m tox --notest" - - -before_build: -- "python -m pip install wheel" - -build_script: -- python -m setup bdist_wheel - -test_script: -- tox - -on_finish: -- ps: >- - if($env:APPVEYOR_RDP_DEBUG -eq 'True') { - $blockRdp = $true - iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) - } -- 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)) - -artifacts: -- path: dist\* - -... diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 4ef33d1a4..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,56 +0,0 @@ ---- - -version: 2 -jobs: - macos-build: - macos: - xcode: "12.2.0" - - steps: - - run: brew install pyenv readline xz - - - run: |- - # https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables - echo ' - export PYENV_ROOT="$HOME/.pyenv" - export PATH="$PYENV_ROOT/bin:$PATH" - ' >> $BASH_ENV - - - run: |- - for py_ver in 3.7.0 3.6.4 - do - pyenv install "$py_ver" & - done - wait - - run: pyenv global 3.7.0 3.6.4 - - - run: python3 -m pip install --upgrade pip wheel - - run: python3 -m pip install tox tox-pyenv - - checkout - - run: tox -e py36,py37 -- -p no:sugar - - store_test_results: - path: .test-results - - store_artifacts: - path: .test-results - - linux-build: - docker: - - image: randomknowledge/docker-pyenv-tox - - steps: - - checkout - - run: pip install tox - - run: tox -e py36,py37 - - store_test_results: - path: .test-results - - store_artifacts: - path: .test-results - -workflows: - version: 2 - test-linux-and-macos: - jobs: - - macos-build - - linux-build - -... diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9667523bc..000000000 --- a/.travis.yml +++ /dev/null @@ -1,174 +0,0 @@ -# yamllint disable rule:line-length ---- - -conditions: v1 - -language: python -os: linux -dist: focal -services: -- memcached -_base_envs: -- &stage_lint - stage: &stage_lint_name lint -- &stage_test - stage: &stage_test_name test -- &stage_test_priority - stage: &stage_test_priority_name test against latest Python versions first (under GNU/Linux) -- &stage_test_osx - stage: &stage_test_osx_name test under OS X (last chance to fail before deploy available) -- &stage_deploy - stage: &stage_deploy_name upload new version of python package to PYPI (only for tagged commits) -- _conditions: - - &condition_api_or_cron - if: type IN (api, cron) -- &no_memcached - services: [] -- &pyenv_base - <<: *stage_test - language: generic - env: - - &env_pyenv PYENV_ROOT="$HOME/.pyenv" - - &env_path PATH="$PYENV_ROOT/bin:$PATH" - before_install: - - &ensure_pyenv_installed | - if [ ! -f "$PYENV_ROOT/bin/pyenv" ] - then - rm -rf "$PYENV_ROOT" - curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash - fi - eval "$(pyenv init -)" - eval "$(pyenv virtualenv-init -)" - pyenv update - - &install_python pyenv install --skip-existing --keep --verbose "$PYTHON_VERSION" - - &switch_python pyenv shell "$PYTHON_VERSION" - - &python_version python --version -- &osx_python_base - <<: *pyenv_base - <<: *stage_test_osx - os: osx - language: generic - before_install: - - brew update - - brew install pyenv || brew upgrade pyenv - - &ensure_pyenv_preloaded | - eval "$(pyenv init -)" - eval "$(pyenv virtualenv-init -)" - - *install_python - - *switch_python - - *python_version - before_cache: - - brew --cache - script: - - travis_retry python -m tox -- &python_3_9_mixture - python: &mainstream_python 3.9 - group: -- &pure_python_base - <<: *stage_test - <<: *python_3_9_mixture -- &pure_python_base_priority - <<: *pure_python_base - <<: *stage_test_priority -- &lint_python_base - <<: *stage_lint - <<: *no_memcached - python: 3.9 - after_failure: skip -python: -- 3.7-dev -jobs: - fast_finish: true - include: - - <<: *lint_python_base - env: TOXENV=pre-commit - - <<: *lint_python_base - env: TOXENV=pre-commit-pep257 - - <<: *lint_python_base - env: TOXENV=dist-check - - <<: *lint_python_base - env: TOXENV=setup-check - - <<: *lint_python_base - name: >- - Ensure that docs get built - (non-strict until \#1797 get fixed) - env: TOXENV=build-docs - - <<: *pure_python_base_priority - # mainstream Python here - - <<: *pure_python_base_priority - # mainstream Python here - # run tests against the bleeding-edge cheroot - env: TOXENV=cheroot-master - - <<: *pure_python_base_priority - python: nightly - - <<: *osx_python_base - python: *mainstream_python - env: - - PYTHON_VERSION=3.6.5 - - *env_pyenv - - *env_path - - <<: *osx_python_base - python: 3.7-dev - env: - - PYTHON_VERSION=3.7-dev - - *env_pyenv - - *env_path - - <<: *osx_python_base - # mainstream Python here - python: *mainstream_python - env: - - PYTHON_VERSION=3.7.0 - # run tests against the bleeding-edge cheroot - - TOXENV=cheroot-master - - *env_pyenv - - *env_path - - <<: *stage_deploy - <<: *python_3_9_mixture - <<: *no_memcached - install: skip - script: skip - deploy: - provider: pypi - skip_cleanup: true - on: - tags: true - all_branches: true - python: *mainstream_python - user: __token__ - distributions: dists - password: - secure: r9jZVhWnwBpbQwkoAQnhcQajV6Hk8WKs53+P20YrNfLSrSfODmJFyljCLsUJH7TnmAdrnQfV19PXPfVXPucK2ZEg2E91/5z6pgADi01NX3QMr7vEpffk6ix0uHBSa3VMBF+VlmhCzAFnNIN0E7M4kjoc5Cr7qBWPwZpqd715axYxBKSIH4Cmt2cyW3ozMftNtbI+ujs+kJTX6m/2UAL8yngau0TWR5bUBaywTZdkfPIKxt2XDfTW5PuOTRgS6QSU+/Va+M1IJhFPthjmTO+t8U/qonSLA34nLkT7vJmME0lyQF0lr+IV41IKxEFz29hmzLY1dyZI5+bs3vEhxU1xGqwr1Hnif6f14TzeiubQrCxt1UP9D3HXguCNI4gGeny1OPJNNt5ezJDNha2HlIy2quLKgtW38TS0PPm7PDqgYhjidZyRXZ8G6A/DAwh00amCNkSN6lG7Lryd1QB44mYHCKm8XdLfBT94EqzSdgQyyoUAA87A8zB5zpHiRD2DGwrTxHkGQo7TTSVr82cYwkRW0nqE6bZkfNTrGLULB8872ZFGpbSbrAft5mDlSnprLRwrEA0SsFUd4O2W64pcvcJENa/NY+vvXAyd9jaHb5v0RpxUyllLrAIFuLEFHeHwyBAlMgq54dtYWqYa8pyJNoUiwOt158qzOE6wnoburP4KA9c= -cache: - pip: true - directories: - - $HOME/.cache/pre-commit - - $HOME/Library/Caches/Homebrew -install: -- python -m pip install tox "setuptools>=28.2" -- python -m tox --notest # Pre-populate a virtualenv with dependencies -script: -- python -m tox -after_failure: -- echo "Here's a list of installed Python packages:" -- pip list --format=columns -- echo Dumping logs, because tests failed to succeed -- | - for log in `ls cherrypy/test/*.log` - do - echo Outputting $log - cat $log - done -- py_log=/home/travis/build/cherrypy/cherrypy/.tox/python/log/python-0.log -- echo Outputting python invocation log from $py_log -- cat $py_log -stages: -- *stage_lint_name -- *stage_test_priority_name -- name: *stage_test_name - <<: *condition_api_or_cron -- name: *stage_test_osx_name - <<: *condition_api_or_cron -- name: *stage_deploy_name - if: tag IS present - -... diff --git a/.zuul.yaml b/.zuul.yaml deleted file mode 100644 index 5f57bc1a8..000000000 --- a/.zuul.yaml +++ /dev/null @@ -1,78 +0,0 @@ ---- -- job: - name: tox-build-docs - parent: tox - vars: - tox_envlist: build-docs - -- job: - name: tox-dist-check - parent: tox - vars: - tox_envlist: dist-check - -- job: - name: tox-pre-commit - parent: tox - vars: - tox_envlist: pre-commit - -- job: - name: tox-pre-commit-pep257 - parent: tox - voting: false - vars: - tox_envlist: pre-commit-pep257 - -- job: - name: tox-py27-setup-check - vars: - tox_envlist: setup-check - python_version: 2.7 - python_use_pyenv: false - parent: tox - -- job: - name: tox-py35-setup-check - vars: - tox_envlist: setup-check - python_version: 3.5 - python_use_pyenv: true - parent: tox - -- job: - name: tox-py36-setup-check - vars: - tox_envlist: setup-check - python_version: 3.6 - python_use_pyenv: true - parent: tox - -- job: - name: tox-py37-setup-check - vars: - tox_envlist: setup-check - python_version: 3.7 - python_use_pyenv: false - parent: tox - -- job: - name: tox-py38-setup-check - vars: - tox_envlist: setup-check - python_version: 3.8 - python_use_pyenv: true - parent: tox - -- project: - check: - jobs: - - tox-build-docs - - tox-dist-check - - tox-pre-commit - - tox-pre-commit-pep257 - - tox-py27-setup-check - - tox-py35-setup-check - - tox-py36-setup-check - - tox-py37-setup-check - - tox-py38-setup-check diff --git a/README.rst b/README.rst index 1a2892a8d..908ae13de 100644 --- a/README.rst +++ b/README.rst @@ -29,15 +29,6 @@ .. image:: https://img.shields.io/gitter/room/cherrypy/cherrypy.svg :target: https://gitter.im/cherrypy/cherrypy -.. image:: https://img.shields.io/travis/cherrypy/cherrypy/master.svg?label=Linux%20build%20%40%20Travis%20CI - :target: https://travis-ci.org/cherrypy/cherrypy - -.. image:: https://circleci.com/gh/cherrypy/cherrypy/tree/master.svg?style=svg - :target: https://circleci.com/gh/cherrypy/cherrypy/tree/master - -.. image:: https://img.shields.io/appveyor/ci/CherryPy/cherrypy/master.svg?label=Windows%20build%20%40%20Appveyor - :target: https://ci.appveyor.com/project/CherryPy/cherrypy/branch/master - .. image:: https://img.shields.io/badge/license-BSD-blue.svg?maxAge=3600 :target: https://pypi.org/project/cheroot diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py index b04701cd6..06fa74507 100644 --- a/cherrypy/test/test_states.py +++ b/cherrypy/test/test_states.py @@ -222,12 +222,6 @@ def test_2_KeyboardInterrupt(self): self.assertEqual(db_connection.running, False) self.assertEqual(len(db_connection.threads), 0) - @pytest.mark.xfail( - 'sys.platform == "Darwin" ' - 'and sys.version_info > (3, 7) ' - 'and os.environ["TRAVIS"]', - reason='https://github.com/cherrypy/cherrypy/issues/1693', - ) def test_4_Autoreload(self): # If test_3 has not been executed, the server won't be stopped, # so we'll have to do it. diff --git a/tox.ini b/tox.ini index 20b291111..3632a5ac0 100644 --- a/tox.ini +++ b/tox.ini @@ -97,12 +97,6 @@ commands_post = passenv = WEBTEST_INTERACTIVE CI - TRAVIS - TRAVIS_* - APPVEYOR - APPVEYOR_* - CIRCLECI - CIRCLE_* setenv = WEBTEST_INTERACTIVE=false extras = From 8fc55c74eca0409b6f23b256d00fce1a7b29d801 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 20 Nov 2025 01:16:45 +0100 Subject: [PATCH 320/322] =?UTF-8?q?=F0=9F=A7=AA=20Drop=20the=20hardcoded?= =?UTF-8?q?=20`.test-results/`=20dir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 --- .test-results/pytest/.gitignore | 1 - pytest.ini | 6 ------ 3 files changed, 10 deletions(-) delete mode 100644 .test-results/pytest/.gitignore diff --git a/.gitignore b/.gitignore index f4e7a0ab0..0cb81681b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,9 +31,6 @@ sphinx/source/_build .project .pydevproject -# test results in junit format for Appveyor -/.test-results/ - # coverage results /.coverage /coverage.xml diff --git a/.test-results/pytest/.gitignore b/.test-results/pytest/.gitignore deleted file mode 100644 index 72e8ffc0d..000000000 --- a/.test-results/pytest/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/pytest.ini b/pytest.ini index 54e25d3f4..6ee0a59c1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -24,9 +24,6 @@ addopts = # https://docs.pytest.org/en/stable/doctest.html --doctest-modules - # Dump the test results in junit format: - --junitxml=.test-results/pytest/results.xml - # `pytest-cov`: # `pytest-cov`, "-p" preloads the module early: -p pytest_cov @@ -34,9 +31,6 @@ addopts = --cov=cherrypy --cov-branch --cov-report=term-missing:skip-covered - --cov-report=html:.tox/tmp/test-results/pytest/cov/ - --cov-report=xml - # --cov-report xml:.test-results/pytest/cov.xml # alternatively move it here --cov-context=test --cov-config=.coveragerc From f2e7a93164501f7bdd0cce2ac91f043aa6ad5d4c Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 20 Nov 2025 01:36:11 +0100 Subject: [PATCH 321/322] Ignore parallel coverage db leftovers in Git --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0cb81681b..6f863a506 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ sphinx/source/_build # coverage results /.coverage +/.coverage.* /coverage.xml # test artifacts From 1f75bc9eed8e0e385f64f368bd69f58d96fb8c2b Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 20 Nov 2025 01:36:37 +0100 Subject: [PATCH 322/322] =?UTF-8?q?=F0=9F=A7=AA=20Temporarily=20mark=20`te?= =?UTF-8?q?st=5Fqueue=5Ffull`=20as=20`xfail`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref: https://github.com/cherrypy/cherrypy/issues/2073 --- cherrypy/test/test_conn.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cherrypy/test/test_conn.py b/cherrypy/test/test_conn.py index b9a2cb8b0..5f602e281 100644 --- a/cherrypy/test/test_conn.py +++ b/cherrypy/test/test_conn.py @@ -13,6 +13,8 @@ from cherrypy._cpcompat import HTTPSConnection, ntob, tonative from cherrypy.test import helper +import pytest + timeout = 1 pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' @@ -797,6 +799,10 @@ def upload(self): class LimitedRequestQueueTests(helper.CPWebCase): setup_server = staticmethod(setup_upload_server) + @pytest.mark.xfail( + reason='Cheroot 11 returns HTTP 503 Service Unavailable on overflows: ' + 'https://github.com/cherrypy/cherrypy/issues/2073', + ) def test_queue_full(self): conns = [] overflow_conn = None